Migrar desde API Direct
Lleva parte o todo tu checkout al Web SDK sin cambiar tu backend: mismo /process/, mismos webhooks, misma conciliación.
Para comercios que ya cobran con API Direct server-to-server y quieren mover parte o todo su checkout al SDK de navegador.
Lo primero que hay que saber
Tu contrato de backend no cambia. El SDK hace POST al mismo /api/v1/process/ con la misma
forma de body, devuelve la misma transacción y dispara los mismos webhooks. Tu conciliación, tu
correlación por client_reference, tu handler de webhooks y tu polling a
GET /api/v1/transactions/{id}/ siguen funcionando sin tocarse.
Lo único que cambia es esto:
| Hoy | Con el SDK |
|---|---|
| Llamas a la bóveda para tokenizar la tarjeta | Los campos seguros del SDK la tokenizan — nunca ves la tarjeta |
Armas el body de /process/ y lo envías | pay() lo arma y lo envía |
| Manejas el redirect de 3DS tú mismo | El SDK lo presenta, redirect o embebido |
Todo lo que pasa después de que /process/ responde queda igual.
Elige tu ruta
Dos migraciones viven en esta guía. Solo comparten el paso de preparación, así que lee únicamente la tuya.
| Si quieres | Ve a | Cambios de servidor |
|---|---|---|
| Lanzar Apple Pay primero, sin tocar aún tu checkout actual | Ruta A | Ninguno |
| Mover tarjetas y APMs al navegador, y agregar Apple Pay | Ruta B | Solo si usas tarjetas guardadas |
Son secuenciales, no alternativas. La Ruta A pone el SDK en tu página sin cambiar lo que tienes; la Ruta B mueve después el resto del checkout, un flujo a la vez. Ver A dónde lleva la Ruta A.
Preparación — ambas rutas
Carga el SDK (cliente)
Ambas opciones 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 |
Una 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)
Para API Direct guardas una llave secreta en tu servidor. El SDK necesita tu llave pública, y va en código de navegador. Son llaves distintas; no reutilices la secreta.
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: 'stage', // cambia a 'production' al salir a producción
session: { customer: { email: 'ada@example.com' } },
});
await tonder.init();session.customer lleva la misma identidad que hoy envías como customer en el body de
/process/. Referencia completa de configuración:
createTonder(config).
Ruta A: empieza con Apple Pay
El primer release más pequeño posible: tu tokenización, tus llamadas a /process/ y tu
conciliación se quedan exactamente donde están, y el SDK renderiza un botón cuyo cargo cae en el
endpoint que ya lees.
La integración completa del botón — habilitación y registro de dominios, el chequeo de
disponibilidad, el contenedor, los callbacks de events.payment — está en la guía de
Apple Pay. Lo que importa entender desde la perspectiva de la
migración:
Antes — tú controlas el submit
const token = await tokenize(cardData);
const tx = await fetch('/tu-backend/charge', { method: 'POST', body: /* … */ });
handleResult(tx);Después — el SDK controla el toque, tú recibes el resultado
Apple exige que la hoja de pago se abra en el mismo tick que el toque, así que el SDK es dueño del
clic y el resultado llega por los callbacks de events.payment en lugar de una promesa. Esos
callbacks no son un mecanismo de Apple Pay: se disparan para todos los métodos que el SDK cobra.
Cuando después muevas tarjetas y APMs, pay() devuelve una promesa y dispara los mismos
callbacks, así que un solo conjunto de handlers sigue cubriendo todo. Puedes ver el botón real
funcionando en el demo de Apple Pay
(ábrelo en Safari).
Tu conciliación queda como está (servidor)
Nada que hacer. El cargo de Apple Pay cae en /api/v1/process/ como tus cargos con tarjeta,
produce una transacción con la misma forma y dispara el mismo webhook. Tu handler existente ya lo
cubre. Envía client_reference en el payment del botón exactamente como hoy y tu correlación
sigue funcionando.
pay({ payment_method: { type: 'apple_pay' } }) se rechaza a propósito — el requisito de gesto de
Apple es la razón. Usa el componente de botón, como muestra la guía de
Apple Pay.
A dónde lleva la Ruta A
El SDK ya está cargado e inicializado en tu página, así que los pasos que quedan son más chicos que el que acabas de dar.
- Captura de tarjeta — el PAN y el CVV dejan de tocar tu JavaScript, lo que saca tu código de esa parte del alcance PCI, y tu llamada de tokenización a la bóveda desaparece. Ruta B, paso 1.
- APMs — mismos códigos de método, una llamada en lugar de un request que armas. Ruta B, paso 3.
Cada uno es un release aparte. No hay un corte único.
Ruta B: mueve tu checkout al SDK
1. Reemplaza la tokenización con campos seguros (cliente)
Este es el paso que elimina código en lugar de agregarlo. Hoy capturas los datos de tarjeta y llamas a la bóveda tú mismo; eso desaparece.
Antes
const tokens = await vault.tokenize({
card_number, cvv, expiration_month, expiration_year, cardholder_name,
});Después — tus <input> se vuelven contenedores vacíos y el SDK monta un iframe seguro en cada
uno
<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();Esos son los ids por defecto; cada campo configurado necesita su contenedor presente antes de
mount(), o la llamada se rechaza con MOUNT_COLLECT_ERROR. Dales un max-height en tu CSS para
que el iframe no crezca antes de asentarse.
Dos lugares distintos configuran estos campos, y vale la pena distinguirlos desde el principio:
| Lo que quieres | Dónde va |
|---|---|
| Ids de contenedor propios, montar un subconjunto de campos, eventos por campo | tonder.create('card_fields', options) — ver la referencia |
| Labels, placeholders, estilos, mensajes de validación | customization.card_fields en createTonder() |
Este es el cambio relevante para PCI. El PAN y el CVV dejan de tocar tu JavaScript por completo — van directo del comprador al iframe seguro de Tonder.
2. Reemplaza el POST a /process/ con pay() (cliente)
Los campos que envías son los mismos de hoy. Solo cambia quién llama.
Antes — tu servidor arma el sobre
{
"operation_type": "payment",
"amount": 250.00,
"currency": "MXN",
"client_reference": "ORD-001",
"customer": { "name": "Ada Lovelace", "email": "ada@example.com" },
"payment_method": { "type": "CARD", "card_number": "<tokenizado>" },
"return_url": "https://merchant.example.com/return"
}Después — lo hace el navegador, con los mismos valores
const transaction = await tonder.pay({
amount: 250,
currency: 'MXN',
client_reference: 'ORD-001',
return_url: 'https://merchant.example.com/return',
payment_method: { type: 'card' },
});Tres diferencias que vale la pena notar, todas simplificaciones:
operation_typedesaparece. El SDK solo crea pagos; los reembolsos y retiros se quedan en tu servidor.customerse movió acreateTonder(). Pertenece a la sesión, no a cada cargo.- Los campos de tarjeta salen del body. El SDK los toma de los campos montados.
Referencia campo por campo, incluyendo idempotency_key y metadata:
tonder.pay(input).
3. Mapea tus llamadas de APM (cliente)
La misma llamada, distinto payment_method.type. Tus códigos de APM se conservan sin cambios.
Hoy, en /process/ | Con el SDK |
|---|---|
"type": "SPEI" | payment_method: { type: 'spei' } |
"type": "oxxopay" | payment_method: { type: 'oxxopay' } |
"type": "safetypaycash" + apm_config | payment_method: { type: 'safetypaycash', config: { … } } |
SafetyPay sigue necesitando country, channel y bank_ids; el SDK puede listarte los bancos
con getPaymentMethodBanks() en lugar de
que los tengas codificados. Ver
Métodos de pago alternativos.
Los APMs devuelven Pending y se liquidan después, exactamente como hoy. Tu manejo de webhooks no
cambia.
4. Opcional: mueve la presentación de 3DS al SDK (cliente)
Si hoy rediriges al comprador a la página hosted tú mismo, puedes cedérselo al SDK y elegir cómo aparece:
presentation_mode: 'redirect'— el navegador navega, como hoy. Tureturn_urlsigue llegando a donde llega ahora.presentation_mode: 'embedded'— el SDK abre un modal y el comprador nunca sale de tu página.
Ver Modo de presentación.
5. Opcional: tarjetas guardadas (servidor + cliente)
Solo si quieres tarjetas almacenadas. Es la única parte de la Ruta B que requiere un cambio de
servidor: las operaciones con tarjetas guardadas necesitan un secure_token de corta vida acuñado
por tu backend con la llave secreta de Tonder que ya tienes.
Una trampa. Con Card on File habilitado, hasta un pago de tarjeta de una sola vez necesita el
token, porque el SDK guarda la tarjeta como parte del cargo. Es una configuración de la cuenta,
así que el mismo código funciona para un negocio y lanza SECURE_TOKEN_REQUIRED para otro.
El endpoint a construir y qué operaciones necesitan el token: Tarjetas guardadas (secure_token).
6. Agrega Apple Pay (cliente)
Sigue la guía de Apple Pay desde el paso 1. Es el mismo trabajo hayas migrado el resto o no.
Conciliación — no cambies esto
El error más común al mover el checkout al navegador es empezar a confiar en el navegador.
Ya surtes pedidos desde webhooks, porque server-to-server no te dejó otra opción. Sigue haciendo exactamente eso. El SDK devuelve una transacción para que actualices la pantalla, no para que liberes mercancía — un navegador se puede cerrar o perder señal, y nada de eso cambia lo que pasó con el dinero.
client_reference— síguelo enviando, sigue correlacionando con élidempotency_key— síguelo enviando, para que un cargo reintentado no se vuelva dos- Webhooks — mismo payload, mismo handler, sin envoltorio. Ver Cómo funcionan los webhooks
getTransaction()— el equivalente en navegador de tuGET /api/v1/transactions/{id}/, para páginas de retorno y consultas puntuales
Prueba la migración
Corre esto en stage antes de cambiar el tráfico de producción.
| Prueba | Qué demuestra que funcionó |
|---|---|
| Un pago con tarjeta | La misma forma de transacción en tu handler existente que antes de migrar |
| Una tarjeta rechazada | Llega como transacción con estado declinado, no como error lanzado |
| Una tarjeta con 3DS | Regresa a tu return_url, o se resuelve en el modal si es embebido |
| Un APM | Devuelve Pending y se liquida por webhook, como hoy |
| Tu handler de webhooks | El código sin tocar sigue procesando los pagos creados por el SDK |
| Apple Pay, si lo adoptaste | La hoja abre en un dispositivo real — el Simulador de iOS no puede probar Apple Pay web |
| Un rechazo de Apple Pay | Llega en on_completed con estado declinado, no en on_error |
Esa última fila es la que sorprende: on_completed significa que el cargo llegó a una respuesta
final, no que la respuesta fue sí.
Qué se queda en tu servidor
El SDK no reemplaza esto. Siguen siendo llamadas de API Direct:
- Reembolsos —
operation_type: "refund" - Retiros —
operation_type: "withdrawal" - Cualquier cargo que crees sin un navegador presente
