Web
Integra el Web SDK: campos de tarjeta seguros, tarjetas guardadas y métodos alternativos.
El SDK Web de Tonder (@tonder.io/web-sdk) es
un SDK de navegador en TypeScript para aceptar pagos: campos de tarjeta seguros, pagos con tarjeta
nueva y guardada, presentación hosted/3DS, descubrimiento de métodos de pago, consulta de
transacciones y respuestas compatibles con webhooks.
¿Prefieres que un agente de IA haga la integración por ti? Instala el plugin Tonder Web SDK en Claude Code, Claude Desktop o Codex.
Antes de empezar
Necesitas:
- Tu llave API pública de Tonder. Nunca pongas llaves secretas en código de navegador.
- Un navegador moderno: Chrome, Safari, Firefox o Edge.
- Un endpoint en tu servidor que genere un
secure_tokende corta vida para usuarios autenticados, si usarás tarjetas guardadas / Card-on-File. - Un endpoint de webhooks para confirmar pagos de forma confiable.
Instalación
npm install @tonder.io/web-sdkimport { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';Sin bundler, carga el build global del navegador desde el CDN del entorno:
| Entorno | URL del CDN |
|---|---|
| Stage | https://zplit-stage.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js |
| Producción | https://zplit-prod.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js |
<script src="https://zplit-stage.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js"></script>
<script>
const { createTonder } = window.Tonder;
</script>En un proyecto TypeScript que usa el CDN, puedes instalar @tonder.io/web-sdk como
devDependency solo para los tipos (npm install -D @tonder.io/web-sdk + import type),
manteniendo el runtime en el CDN. No importes código de runtime del paquete en ese caso.
Inicio rápido: pago con tarjeta
Agrega los contenedores para los campos de tarjeta
<form id="checkout-form">
<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 type="submit">Pagar</button>
</form>Limita la altura de cada contenedor para que el iframe seguro no crezca visualmente mientras se acomoda al layout:
.card-field {
width: 100%;
max-height: 90px;
}Inicializa, monta y paga
import { createTonder } from '@tonder.io/web-sdk';
const tonder = createTonder({
api_key: 'pk_test_...',
environment: 'sandbox',
session: {
customer: {
email: 'ada@example.com',
first_name: 'Ada',
last_name: 'Lovelace',
},
},
});
await tonder.init();
const card_fields = tonder.create('card_fields');
await card_fields.mount();
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
metadata: { cart_id: 'cart_789' },
payment_method: { type: 'card' },
});Maneja el resultado
if (transaction.status === 'Success' || transaction.status === 'Authorized') {
// Muestra la confirmación.
} else if (transaction.status === 'Pending') {
// El cliente puede necesitar completar 3DS o un método asíncrono.
// Confirma el estado final con webhooks o getTransaction().
} else {
// Muestra un mensaje de pago recuperable.
console.warn(transaction.decline_code, transaction.decline_reason);
}Los rechazos no se lanzan como error — se devuelven como transacciones: lee
transaction.status. Los fallos del SDK sí se lanzan como AppError (consulta
Errores).
Los montos del SDK Web son unidades decimales (150 = MXN 150.00). Consulta
Dinero, monedas y montos.
Configuración
createTonder(config) crea una instancia para un comprador/sesión. Recrea el SDK si cambia el
cliente, el secure_token o el entorno.
| Campo | Requerido | Descripción |
|---|---|---|
api_key | Sí | Llave pública de Tonder para integraciones de navegador. |
environment | Sí | 'sandbox', 'stage' o 'production'. |
session.customer | Para pay() y tarjetas guardadas | Identidad del cliente. Omítelo en páginas de retorno de solo lectura que solo llaman getTransaction(). |
session.customer.phone | Para tarjetas guardadas, en negocios configurados para requerir teléfono | Teléfono del cliente en formato E.164 (+52 + 10 dígitos). No reemplaza al email, que siempre identifica al cliente. Consulta Negocios que requieren teléfono para tarjetas guardadas. |
session.secure_token | Para tarjetas guardadas | Token de corta vida generado por tu backend. |
presentation_mode | No | 'redirect' por defecto, o 'embedded' para el modal del SDK. |
events.presentation | No | Callbacks on_open / on_close de la vista hosted embebida. |
customization.card_fields | No | Etiquetas, placeholders, estilos y mensajes de validación de los campos seguros. |
La tabla completa de personalización (estilos por campo, mensajes de error, icono de la tarjeta) está en la Referencia SDK · Web.
Modo de presentación
Cuando un pago requiere un paso hosted (3DS, instrucciones de APM), el SDK usa
presentation_mode:
| Modo | Comportamiento |
|---|---|
redirect | El navegador navega a la página hosted. Usa return_url, getTransaction() y webhooks para confirmar el estado final. |
embedded | El SDK abre un modal a pantalla completa. El 3DS de tarjeta espera la transacción final; las instrucciones hosted de APM/SPEI pueden devolver Pending de inmediato. |
Tarjetas guardadas (secure_token)
Las operaciones con tarjetas guardadas (getCustomerCards(), enrollCard(),
removeCustomerCard(), pagar con saved_card) requieren session.customer y
session.secure_token. Genera el token llamando a /api/secure-token/ desde tu backend con
tu llave secreta:
Las tarjetas guardadas / Card-on-File requieren @tonder.io/web-sdk 0.1.11 o posterior.
fetch("https://stage.tonder.io/api/secure-token/", {
method: 'POST',
headers: {
'Authorization': 'Token YOUR_SECRET_KEY',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
const secureToken = result.access;
// Pásalo al frontend como session.secure_token
});fetch("https://app.tonder.io/api/secure-token/", {
method: 'POST',
headers: {
'Authorization': 'Token YOUR_SECRET_KEY',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
const secureToken = result.access;
});El secure_token generado es válido por 1 hora. Úsalo dentro de ese plazo; si lo cacheas o
reutilizas después, genera uno nuevo.
Ofrece tarjetas guardadas solo a usuarios autenticados. Las tarjetas guardadas pertenecen al
cliente de session.customer. Construye session.customer (email y, cuando se requiera, teléfono)
en tu servidor a partir de la cuenta autenticada — nunca desde un campo que el comprador pueda
editar — y genera un secure_token solo para sesiones autenticadas. Los checkouts de invitados
deben pagar con tarjeta nueva y omitir secure_token, getCustomerCards() y enrollCard().
Negocios que requieren teléfono para tarjetas guardadas
Las tarjetas guardadas siempre pertenecen al cliente identificado por session.customer.email.
Algunos negocios están configurados por Tonder para requerir además el teléfono del cliente
para las tarjetas guardadas. En esos negocios:
- Pasa
emailyphoneensession.customer. El email identifica al cliente; el teléfono habilita sus tarjetas guardadas. - Sin
phone, las tarjetas guardadas no están disponibles:canSaveCards()devuelvefalse, ygetCustomerCards(),enrollCard()ypay({ payment_method: { type: 'saved_card' } })lanzanSAVE_CARDS_UNAVAILABLEsin enviar ninguna petición. - Un pago con tarjeta nueva (
pay({ payment_method: { type: 'card' } })) sigue funcionando sinphone: la tarjeta se cobra pero no se guarda. - El teléfono no reemplaza al email: el mismo teléfono con otro email es otro cliente y no verá esas tarjetas.
const tonder = createTonder({
api_key: 'pk_test_...',
environment: 'sandbox',
session: {
customer: {
email: 'carlos@example.com', // identifica al cliente
phone: '+525512345678', // requerido para tarjetas guardadas en negocios que piden teléfono
first_name: 'Carlos',
last_name: 'Mendoza',
},
secure_token: await getSecureTokenFromYourBackend(),
},
});
await tonder.init();
if (tonder.canSaveCards()) {
const cards = await tonder.getCustomerCards();
// muestra las tarjetas guardadas y la opción "guardar tarjeta"
} else {
// oculta la lista de tarjetas guardadas y la opción "guardar tarjeta"
}El SDK nunca le pide el teléfono al comprador ni muestra ningún mensaje: toma el teléfono de la
cuenta del usuario autenticado en tu servidor y usa canSaveCards() para decidir si muestras la UI
de tarjetas guardadas.
Formato del número de teléfono
Envía el teléfono en formato E.164 (+52 seguido de 10 dígitos) y envía siempre el mismo valor
para el mismo usuario. Tonder no lo normaliza: +525512345678 y 5512345678 no son el mismo valor.
Ejemplo para México:
function normalizeMxPhone(value) {
const d = String(value || '').replace(/\D/g, '');
if (d.length === 10) return '+52' + d; // 5512345678
if (d.length === 12 && d.startsWith('52')) return '+' + d; // 525512345678
if (d.length === 13 && d.startsWith('521')) return '+52' + d.slice(3); // prefijo móvil legacy +521
return null; // inválido: no ofrezcas tarjetas guardadas
}Pagar con una tarjeta guardada
El contenedor del CVV de una tarjeta guardada es #collect-cvv-<card_id> (o pasa el tuyo con
fields: [{ field: 'cvv', container_id: '#my-cvv' }]):
<div id="collect-cvv-CARD_ID"></div>const tonder = createTonder({
api_key: 'pk_test_...',
environment: 'sandbox',
session: {
customer: { email: 'ada@example.com' },
secure_token: await getSecureTokenFromYourBackend(),
},
});
await tonder.init();
const cards = await tonder.getCustomerCards();
const selected_card = cards[0];
// Monta el CVV de la tarjeta guardada solo cuando la tarjeta no puede cobrarse
// por una suscripción Card-on-File existente.
if (!selected_card.subscription_id) {
const cvv = tonder.create('card_fields', {
card_id: selected_card.card_id,
fields: ['cvv'],
});
await cvv.mount();
}
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'saved_card', card_id: selected_card.card_id },
});Si el formulario de tarjeta nueva está en la misma pantalla, mantenlo montado mientras montas el
CVV de la tarjeta guardada — con el unmount_context por defecto ('all') se desmonta:
const cvv = tonder.create('card_fields', {
card_id: selected_card.card_id,
fields: ['cvv'],
unmount_context: 'none', // conserva el formulario de tarjeta nueva si está en la misma pantalla
});Si una tarjeta no tiene subscription_id y su CVV no está montado y capturado, pay() lanza
MOUNT_COLLECT_ERROR.
Guardar una tarjeta nueva
// Requiere session.customer y session.secure_token.
const card_fields = tonder.create('card_fields');
await card_fields.mount();
// Cuando el comprador haya llenado los campos:
const enrollment = await tonder.enrollCard();
// { card_id: '…', subscription_id: '…' } // subscription_id viene cuando Card-on-File está habilitadoenrollCard() guarda la tarjeta sin cobrarla. Al resolverse, los campos de tarjeta nueva se
limpian. Actualiza la lista con getCustomerCards().
Card-on-File en un pago con tarjeta nueva
Cuando Card-on-File está habilitado para tu negocio, un pago con tarjeta nueva también guarda la
tarjeta para session.customer; la próxima vez aparece en getCustomerCards() con un
subscription_id. Muestra el texto de consentimiento correspondiente en tu checkout.
La única excepción: en un negocio que requiere teléfono, un pago con tarjeta nueva sin
session.customer.phone se cobra pero no se guarda — consulta
Negocios que requieren teléfono para tarjetas guardadas.
Flujo recomendado para tarjetas guardadas
- Tu servidor autentica al usuario y genera un
secure_token. Construyesession.customera partir de la cuenta:emailsiempre;phone(E.164) cuando tu negocio lo requiera. createTonder({ session: { customer, secure_token } })→init().- Si
canSaveCards()esfalse, oculta la UI de tarjetas guardadas y ofrece solo tarjeta nueva. getCustomerCards()→ muestracard_scheme, elcard_numberenmascarado y la vigencia.- Monta el formulario de tarjeta nueva para "usar otra tarjeta".
- Si el comprador elige una tarjeta guardada sin
subscription_id, monta su CVV (unmount_context: 'none'). pay()con{ type: 'saved_card', card_id }o{ type: 'card' }.- Ofrece "eliminar tarjeta" con
removeCustomerCard(card_id)y actualiza la lista. - Recrea el SDK cuando cambie el usuario o expire el token (1 hora).
Referencia de la API
Autorización
SecretKeyAuth Your SECRET key (not the public API key), with the word Token and a space in front of it. Example: Token e6440b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a. In this playground you may paste just the key; the prefix is added for you.
In: header
Cuerpo de la respuesta
application/json
curl -X POST "https://example.com/secure-token/"{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzM1Njg5NjAwLCJpYXQiOjE3MzU2ODYwMDAsImp0aSI6IkVYQU1QTEVfSlRJIiwidXNlcl9pZCI6MH0.EXAMPLE_SIGNATURE_NOT_A_REAL_TOKEN"
}Métodos de pago alternativos
Si tu checkout ya sabe qué método ofrecer, pasa el código directamente a pay():
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'oxxopay' },
});Usa getPaymentMethods() (opcional) para renderizar los métodos habilitados de tu negocio, y
getPaymentMethodBanks() para los métodos SafetyPay respaldados por banco:
const banks = await tonder.getPaymentMethodBanks();
const bank = banks.cash[0];
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: {
type: 'safetypayCash',
config: {
country: bank.country, // p. ej. 'Mexico'
channel: bank.channel, // 'WP' efectivo, 'OL' transferencia
bank_ids: [{ id: bank.code }], // p. ej. [{ id: '8186' }]
},
},
});Los métodos APM/SPEI suelen liquidar de forma asíncrona. Usa webhooks para el cumplimiento de pedidos.
Demos
Prueba cada flujo en el portal de demos del SDK:
| Flujo | Demo |
|---|---|
| Pago con tarjeta | web/card-payment |
| Enrolamiento de tarjeta | web/enroll-card |
| Tarjetas guardadas | web/saved-cards |
| Métodos de pago | web/payment-methods |
| Bancos SafetyPay | web/safetypay-banks |
Los demos legacy (Web SDK Lite e Inline) usan versiones anteriores del SDK y se conservan solo como referencia — para integraciones nuevas usa los demos de arriba. Si todavía estás en el SDK anterior, sigue la guía de migración.
Conciliación
client_referencees requerido: es tu referencia de orden y aparece en dashboards, exportes, webhooks y reportes de transacciones.- Usa un
idempotency_keyestable por intento de checkout para que los reintentos no dupliquen cargos. No reutilicesclient_referencecomo llave de idempotencia. - Los webhooks del SDK Web usan el payload plano (campos de nivel superior) con
event_type: payment_Success/payment_Pending. Consulta el catálogo de eventos.
Siguientes pasos
Referencia SDK · Web
Todos los métodos, tipos, errores y estados de pago.
Portal de demos de SDK
Demos interactivas por flujo + demos legacy (Lite e Inline).
Plugin Tonder Web SDK (IA)
Deja que Claude Code o Codex hagan esta integración por ti.
Reembolsos — desde tu backend
Reembolsa un pago con tarjeta con una sola llamada server-side.
Webhooks
Confirma pagos de forma confiable en tu backend.
