SDKs

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_token de 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-sdk
import { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';

Sin bundler, carga el build global del navegador desde el CDN del entorno:

EntornoURL del CDN
Stagehttps://zplit-stage.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js
Producciónhttps://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.

CampoRequeridoDescripción
api_keySíLlave pública de Tonder para integraciones de navegador.
environmentSí'sandbox', 'stage' o 'production'.
session.customerPara pay() y tarjetas guardadasIdentidad del cliente. Omítelo en páginas de retorno de solo lectura que solo llaman getTransaction().
session.customer.phonePara tarjetas guardadas, en negocios configurados para requerir teléfonoTelé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_tokenPara tarjetas guardadasToken de corta vida generado por tu backend.
presentation_modeNo'redirect' por defecto, o 'embedded' para el modal del SDK.
events.presentationNoCallbacks on_open / on_close de la vista hosted embebida.
customization.card_fieldsNoEtiquetas, 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:

ModoComportamiento
redirectEl navegador navega a la página hosted. Usa return_url, getTransaction() y webhooks para confirmar el estado final.
embeddedEl 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 email y phone en session.customer. El email identifica al cliente; el teléfono habilita sus tarjetas guardadas.
  • Sin phone, las tarjetas guardadas no están disponibles: canSaveCards() devuelve false, y getCustomerCards(), enrollCard() y pay({ payment_method: { type: 'saved_card' } }) lanzan SAVE_CARDS_UNAVAILABLE sin enviar ninguna petición.
  • Un pago con tarjeta nueva (pay({ payment_method: { type: 'card' } })) sigue funcionando sin phone: 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á habilitado

enrollCard() 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

  1. Tu servidor autentica al usuario y genera un secure_token. Construye session.customer a partir de la cuenta: email siempre; phone (E.164) cuando tu negocio lo requiera.
  2. createTonder({ session: { customer, secure_token } }) → init().
  3. Si canSaveCards() es false, oculta la UI de tarjetas guardadas y ofrece solo tarjeta nueva.
  4. getCustomerCards() → muestra card_scheme, el card_number enmascarado y la vigencia.
  5. Monta el formulario de tarjeta nueva para "usar otra tarjeta".
  6. Si el comprador elige una tarjeta guardada sin subscription_id, monta su CVV (unmount_context: 'none').
  7. pay() con { type: 'saved_card', card_id } o { type: 'card' }.
  8. Ofrece "eliminar tarjeta" con removeCustomerCard(card_id) y actualiza la lista.
  9. Recrea el SDK cuando cambie el usuario o expire el token (1 hora).

Referencia de la API

POST
/secure-token/

Autorización

SecretKeyAuth
Authorization<token>

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:

FlujoDemo
Pago con tarjetaweb/card-payment
Enrolamiento de tarjetaweb/enroll-card
Tarjetas guardadasweb/saved-cards
Métodos de pagoweb/payment-methods
Bancos SafetyPayweb/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_reference es requerido: es tu referencia de orden y aparece en dashboards, exportes, webhooks y reportes de transacciones.
  • Usa un idempotency_key estable por intento de checkout para que los reintentos no dupliquen cargos. No reutilices client_reference como 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

¿Te resultó útil esta página?

En esta página