Migrar del SDK legacy
De tonder-web-sdk v2 (InlineCheckout / LiteInlineCheckout) a @tonder.io/web-sdk, incluyendo el cambio de webhooks.
Para comercios en tonder-web-sdk v2 — InlineCheckout o LiteInlineCheckout — que migran a
@tonder.io/web-sdk.
Lee esto antes de planear el trabajo
Esta migración no es solo un cambio de front-end. Tu handler de webhooks también tiene que cambiar, y esa es la parte que los comercios subestiman al planear.
El SDK nuevo cobra a través de un API de pagos de Tonder distinto al del SDK legacy, así que los eventos que recibe tu servidor traen otro payload. No es un renombre de campos: algunos desaparecen, otros son nuevos, y si aceptas APMs tus dos handlers actuales se vuelven uno. Presupuesta tiempo de backend — ver Webhooks.
Todo lo demás es un movimiento función por función. Tarjetas guardadas, Card on File, enrolamiento, re-captura de CVV, APMs, SafetyPay, 3DS y campos seguros existen en el SDK nuevo.
¿Qué clase importabas?
| Usabas | Tu migración es | Ve a |
|---|---|---|
LiteInlineCheckout | Casi todo renombres de métodos — la UI ya es tuya | Ruta A |
InlineCheckout | Construyes la UI de checkout que el SDK viejo dibujaba | Ruta B |
Ambas rutas comparten Webhooks, que es el mismo trabajo en cualquiera de las dos.
Preparación — ambas rutas
Carga el SDK (cliente)
El SDK legacy se distribuía como paquete de npm y como script tag. Este también, y no estás atado a la opción que usabas antes — ambas funcionan en cualquier framework, React y Next.js incluidos.
| Las actualizaciones te llegan | |
|---|---|
CDN <script> — el SDK llega como window.Tonder | Automáticamente; la URL sigue un canal de versión mayor (/web-sdk/v1/) |
npm — npm install @tonder.io/web-sdk | Cuando subes la versión y despliegas |
Si estabas en npm, elimina el paquete viejo para que los dos no puedan cargarse a la vez:
npm uninstall tonder-web-sdkUna app en TypeScript puede instalar el paquete de npm como devDependency solo por los tipos y
mantener el runtime por CDN.
Snippets de instalación y CDN: Instalación.
Crea la instancia (cliente)
Antes — una clase que construyes, luego configuras, luego usas para cobrar
const checkout = new LiteInlineCheckout({
mode: 'stage',
apiKey: 'YOUR_KEY',
returnUrl: 'https://merchant.example.com/return',
callBack: (result) => handleResult(result),
});
await checkout.injectCheckout();
checkout.configureCheckout({
customer: { firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com' },
order_reference: 'ORD-001',
});Después — un factory, y el cliente pertenece a la sesión
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: 'stage', // cambia a 'production' al salir a producción
session: {
customer: { email: 'ada@example.com', first_name: 'Ada', last_name: 'Lovelace' },
},
});
await tonder.init();| Legacy | Nuevo | Nota |
|---|---|---|
mode: 'stage' | 'production' | environment: 'stage' | 'production' | Mismos dos valores, llave renombrada |
apiKey | api_key | La misma llave pública |
injectCheckout() | init() | |
configureCheckout({ customer }) | session.customer al crear | Ya no es una llamada aparte |
returnUrl en el constructor | return_url por llamada a pay() | Permite URLs de retorno por transacción |
callBack | events.payment — o la promesa que devuelve pay() | Ver abajo |
order_reference | client_reference | Renombrado; tu correlación se conserva. Ver Correlación |
Referencia completa: createTonder(config).
Resultados: callback o promesa (cliente)
El callBack legacy se disparaba para todo. El SDK nuevo te da ambos canales, y no son
alternativas:
pay()devuelve una promesa con la transacción — úsala para actualizar la pantalla.events.paymentse dispara para todos los métodos, incluyendo flujos sin promesa. Un solo conjunto de handlers cubre el checkout completo.
Un rechazo no es un error en ninguno de los dos canales: llega como una transacción con estado
declinado, en on_completed. on_error significa que no existe transacción alguna. Detalle de
eventos en el README del SDK.
Ruta A: desde LiteInlineCheckout
La UI ya es tuya, así que esto es un pase de renombres más un cambio de comportamiento.
1. Mapea los métodos (cliente)
| Legacy | Nuevo |
|---|---|
injectCheckout() | init() |
mountCardFields(request) | tonder.create('card_fields', options).mount() |
unmountCardFields(context) | card_fields.unmount() |
revealCardFields(request) | card_fields.reveal(input) |
getCustomerCards() | getCustomerCards() |
saveCustomerCard() | enrollCard() |
removeCustomerCard(skyflowId) | removeCustomerCard(card_id) |
getCustomerPaymentMethods() | getPaymentMethods() |
payment(data) | pay(input) |
verify3dsTransaction() | — el SDK resuelve 3DS por sí mismo |
card_id y unmount_context conservan el significado que tenían en mountCardFields,
incluyendo los valores all / none / current.
2. Monta los campos de tarjeta (cliente)
Antes
await checkout.mountCardFields({ /* configuración de campos */ });Después — los ids de contenedor son los mismos defaults que usaba el SDK legacy
<div id="collect-cardholder-name" class="card-field"></div>
<div id="collect-card-number" class="card-field"></div>
<div id="collect-expiration-month" class="card-field"></div>
<div id="collect-expiration-year" class="card-field"></div>
<div id="collect-cvv" class="card-field"></div>const card_fields = tonder.create('card_fields');
await card_fields.mount();Cada campo configurado necesita su contenedor en el DOM antes de mount(), o la llamada se
rechaza con MOUNT_COLLECT_ERROR.
Los estilos, labels y placeholders se mueven a customization.card_fields en createTonder() —
ya no se pasan a la llamada de montaje. Ver
customization.card_fields.
3. Cobra (cliente)
Antes
checkout.configureCheckout({ customer, order_reference: 'ORD-001' });
const result = await checkout.payment({ /* carrito */ });Después
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://merchant.example.com/return',
client_reference: 'ORD-001',
payment_method: { type: 'card' },
});Las tarjetas guardadas usan { type: 'saved_card', card_id }; los APMs usan su código de método.
Lista completa de campos: tonder.pay(input).
4. Elimina verify3dsTransaction() (cliente)
El flujo legacy exigía llamarlo en la página de retorno. El SDK nuevo resuelve 3DS por sí mismo y te da a elegir la presentación:
presentation_mode: 'redirect'— el navegador navega, como hoypresentation_mode: 'embedded'— un modal, y el comprador nunca sale de tu página
Ver Modo de presentación.
5. Opcional: agrega Apple Pay (cliente)
No existía en el SDK legacy, así que esto es capacidad nueva más que migración.
Hay algo que vale la pena iniciar ahora y no al final: pide a Tonder habilitar Apple Pay y registrar tus dominios. Tonder trata con Apple — tú nunca los contactas y no necesitas cuenta de desarrollador de Apple. Tu parte es enviar la lista de dominios y alojar un archivo que Tonder te da. No es un paso de código, pero condiciona todo lo demás, y es la razón más común por la que una integración terminada no funciona en producción.
Después, sigue la guía de Apple Pay.
Ruta B: desde InlineCheckout
InlineCheckout dibujaba el checkout completo: el formulario de tarjeta, la lista de tarjetas
guardadas, el selector de APMs y el estilo alrededor. El SDK nuevo no dibuja un checkout. Tú
construyes la UI; el SDK te da inputs seguros para los datos de tarjeta y la información para
renderizar todo lo demás.
Eso es toda esta migración. Cada capacidad que tenías sigue ahí — lo que cambia es quién la renderiza.
1. Inventaría lo que la UI vieja le daba a tus compradores (planeación)
Antes de escribir código, lista cuáles de estas cosas mostraba realmente tu checkout. Solo reconstruyes esas.
| La UI vieja dibujaba | Ahora renderizas | El SDK te da |
|---|---|---|
| Formulario de tarjeta | Tu propio layout | Inputs seguros vía create('card_fields') — nunca tocas el PAN |
| Lista de tarjetas guardadas | Tu propia lista y selector | getCustomerCards() — número enmascarado, marca, expiración, subscription_id |
| Checkbox de guardar tarjeta | Tu propio checkbox | enrollCard() |
| Selector de APMs | Tu propia lista | getPaymentMethods() — incluye el label y la URL del logo de cada método |
| Selector de banco SafetyPay | Tu propio selector | getPaymentMethodBanks() — agrupados en cash y transfer |
| Botón de pagar, textos, colores | Tuyos | — |
| Estados de carga y error | Tuyos | Códigos de error para ramificar |
No tienes que diseñar iconos de métodos de pago. getPaymentMethods() devuelve una URL de
logo por método — es lo que la UI vieja renderizaba.
2. Reemplaza los tres métodos de ciclo de vida (cliente)
Antes — el SDK era dueño de la pantalla
const checkout = new InlineCheckout({ mode, apiKey, returnUrl, callBack });
await checkout.injectCheckout(); // dibujaba todo dentro de tu contenedor
checkout.setCallback(handleResult);
checkout.removeCheckout();Después — tú eres dueño de la pantalla; el SDK, de la seguridad de la tarjeta y del cobro
Tenías un contenedor y el SDK lo llenaba con un checkout entero. Ahora tú armas tu formulario, y el SDK monta un iframe seguro dentro de cada input de tarjeta. Todo lo demás — labels, el botón de pagar, la lista de tarjetas guardadas, el selector de APMs — es tu markup.
<!-- tu formulario, tu layout; solo estos cinco son del SDK -->
<div id="collect-cardholder-name" class="card-field"></div>
<div id="collect-card-number" class="card-field"></div>
<div id="collect-expiration-month" class="card-field"></div>
<div id="collect-expiration-year" class="card-field"></div>
<div id="collect-cvv" class="card-field"></div>
<button id="pay">Pagar</button>const tonder = createTonder({ /* …ver Preparación… */ });
await tonder.init();
const card_fields = tonder.create('card_fields');
await card_fields.mount(); // dentro de los contenedores de arriba
const transaction = await tonder.pay({ /* …ver Ruta A, paso 3… */ });
card_fields.unmount(); // tu teardown, en lugar de removeCheckout()Esos cinco ids son los defaults; cada campo configurado necesita su contenedor en el DOM antes de
mount(), o la llamada se rechaza con MOUNT_COLLECT_ERROR. Dales un max-height en tu CSS para
que los iframes no crezcan antes de asentarse.
3. Construye los flujos que inventariaste (cliente)
Son los mismos flujos por los que la UI vieja llevaba a tu comprador:
| Flujo | Guía |
|---|---|
| Tarjeta nueva | Inicio rápido: pago con tarjeta |
| Tarjeta guardada | Pagar con una tarjeta guardada |
| Guardar una tarjeta | Guardar una tarjeta nueva |
| APMs y SafetyPay | Métodos de pago alternativos |
| Apple Pay — nuevo, no existía en el SDK legacy | Apple Pay |
Una regla que vale la pena llevar a tu propia UI: subscription_id en una tarjeta guardada
decide si necesitas CVV. Presente significa cobrar directo; null significa montar primero el
campo de CVV de tarjeta guardada.
4. Estilos (cliente)
El customization viejo tenía dos mitades. Solo una tiene contraparte:
| Customization legacy | Ahora |
|---|---|
| Estilos, labels y placeholders de los campos seguros | customization.card_fields en createTonder() |
| UI del checkout: secciones visibles, colores, texto del botón | Tu propio CSS — no hay equivalente en el SDK |
Webhooks (servidor)
Ambas rutas necesitan esto, y es la parte que no es un renombre.
Hoy recibes dos formas de payload distintas — una para pagos con tarjeta, otra para APMs. Ahora recibirás una sola, la misma para todos los métodos: el formato Short, el mismo que envía API Direct. El detalle de ambos formatos está en el Modelo de webhooks y el catálogo de eventos.
Webhooks de tarjeta
| Campo legacy | Campo nuevo | Nota |
|---|---|---|
transaction_reference | — | Desaparece. Nada del payload nuevo lo reemplaza |
payment_id | — | Desaparece |
checkout_id | — | Desaparece. No hay objeto checkout en la ruta nueva |
| — | id | Nuevo, y el que importa. El identificador de Tonder para la transacción — lo que pasas a getTransaction() y citas a soporte |
| — | transaction_id | Nuevo. Un id interno de Tonder del registro de procesamiento. No es tu llave de correlación |
| — | client_reference | Nuevo. Tu referencia de orden, dentro del webhook. Con esto correlacionas tu pedido |
status y transaction_status | status | El par legacy estaba duplicado; ahora hay uno |
amount, currency, metadata | mismos nombres | Se conservan |
provider | provider | Se conserva |
transaction_type | operation_type | |
operation_date | created | Ahora en ISO 8601 |
number_of_payment_attempts | — | Desaparece |
response (anidado, del procesador) | — | Desaparece. Lee status y los campos de rechazo |
| — | event_type | p. ej. payment_Success |
| — | payment_method_type | p. ej. CARD, SPEI, OXXO |
| — | action | p. ej. MODIFY |
Webhooks de APM
Antes — envuelto, y con forma distinta a los de tarjeta
{
"action": "payment",
"type": "apm",
"data": { "transaction_status": "Success", "payment_id": 41714, "checkout_id": "…" }
}Después — idéntico en forma a un webhook de tarjeta
{
"id": "fc38522e-…",
"operation_type": "payment",
"status": "Success",
"payment_method_type": "SPEI",
"client_reference": "ORD-001",
"transaction_id": "e9340a04-…",
"event_type": "payment_Success"
}Si tu código ramifica en type === 'apm' o desenvuelve data, elimínalo. Un solo handler
cubre ahora todos los métodos.
Qué hacer
- Lee la especificación del payload actual: Cómo funcionan los webhooks y el Modelo de webhooks.
- Correlaciona con
client_referenceen lugar decheckout_idopayment_id. - Mantén el handler idempotente guardando los ids de eventos procesados — la política de reintentos y la Dead Letter Queue están en Entrega y reintentos.
- Si migras gradualmente, mantén ambos endpoints durante el corte; los payloads se distinguen por
la presencia de
event_type.
Correlación (servidor)
Tu referencia de orden sigue funcionando. Se renombra, y sigue identificando lo mismo del lado de Tonder — la conciliación que ya tienes no necesita repensarse, solo cambia el nombre del campo.
| Campo | |
|---|---|
| Legacy | configureCheckout({ order_reference }) |
| Nuevo | pay({ client_reference }) |
metadata existe en ambos, sin cambios.
Revisa esto antes de migrar. Ningún SDK fusiona estos campos — la referencia de orden y
metadata viajan por separado, y siempre lo han hecho. Pero en los reportes exportados de
transacciones, la columna Business Transaction ID prefiere metadata.order_id y solo cae a la
referencia de orden cuando está ausente. Si has enviado ambos con valores distintos, tus webhooks
correlacionan con un identificador y tus reportes con otro. Decide cuál lee realmente tu
conciliación antes de mapearlo, y envía el mismo valor en ambos de aquí en adelante. La tabla
completa de metadata está en tonder.pay(input).
Prueba la migración
Corre esto en stage antes de cambiar el tráfico de producción.
| Prueba | Qué demuestra que funcionó |
|---|---|
| Pago con tarjeta nueva | La transacción se crea vía /process/, no por el router |
| Tarjeta rechazada | Llega como transacción con estado declinado, no como error lanzado |
| 3DS | Se resuelve sin que llames verify3dsTransaction() |
Tarjeta guardada con subscription_id | Cobra sin pedir CVV |
Tarjeta guardada sin subscription_id | Pide CVV y luego cobra |
| Enrolamiento | La tarjeta aparece después en getCustomerCards() |
| Un APM | Devuelve Pending y se liquida por webhook |
| Handler de webhooks | Procesa el payload plano nuevo para una tarjeta Y un APM |
| Correlación | El client_reference de tu orden aparece en el webhook |
Las últimas dos son las que valen una prueba explícita. Todo lo de arriba falla ruidosamente; un descuadre de webhooks falla en silencio, y te enteras cuando un pedido no se surte.
