Browser TypeScript SDK for accepting payments with Tonder. It provides secure card fields, new-card and saved-card payments, hosted/3DS presentation, payment-method discovery, transaction lookup, and webhook-friendly transaction responses.
✨ AI-assisted integration
Want an AI agent to help wire Tonder into your app? Use Tonder AI Integrations to guide Web SDK setup for browser-based web apps, including vanilla HTML, React, Next.js, Angular, and similar frameworks, with secure card fields, saved cards, payment methods, and SafetyPay flows.
- Install
- Before you start
- Backend secure token endpoint
- Quick start: card payment
- Configuration
- Core concepts
- Payment flows
- API reference
createTonder(config)tonder.init()tonder.create('card_fields', options?)card_fields.mount()card_fields.unmount()card_fields.reveal(input)tonder.pay(input)tonder.getTransaction(id)tonder.enrollCard()tonder.getCustomerCards()tonder.removeCustomerCard(card_id)tonder.getPaymentMethods()tonder.getPaymentMethodBanks()
- Types
- Errors
- Payment statuses
- Webhooks
- CDN build
npm install @tonder.io/web-sdkimport { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';No bundler? Use the browser global build. See CDN build.
You need:
- A Tonder public
api_key. Never put secret keys in browser code. - A modern browser: Chrome, Safari, Firefox, or Edge.
- A server endpoint that can create a short-lived
secure_tokenwhen using saved cards/Card on File. - A webhook endpoint for reliable payment fulfillment.
Read deployment-specific SDK values from your app's public browser configuration instead of hardcoding real merchant values in checkout code. The Tonder public API key is safe to expose to the browser, but keeping it in config makes environment switching and key rotation safer.
Use the convention for your framework.
Vite / React:
const tonderPublicConfig = {
api_key: import.meta.env.VITE_TONDER_PUBLIC_API_KEY,
environment: import.meta.env.VITE_TONDER_ENVIRONMENT as
| 'sandbox'
| 'stage'
| 'production',
};Next.js Client Components:
const tonderPublicConfig = {
api_key: process.env.NEXT_PUBLIC_TONDER_PUBLIC_API_KEY,
environment: process.env.NEXT_PUBLIC_TONDER_ENVIRONMENT as
| 'sandbox'
| 'stage'
| 'production',
};Angular:
import { environment } from '../environments/environment';
const tonderPublicConfig = {
api_key: environment.tonderPublicApiKey,
environment: environment.tonderEnvironment,
};Plain HTML / server-rendered config:
const tonderPublicConfig = window.__TONDER_CONFIG__;currency is checkout/business data. Keep it in your checkout state or merchant configuration; it does not need to be an environment variable unless your app already manages it that way.
session.secure_token is required whenever the SDK needs to create, read, update, or remove stored card records for a customer. In practice, this means:
| SDK operation | Needs session.secure_token? |
Why |
|---|---|---|
tonder.enrollCard() |
Yes | Saves a new card for the customer. |
tonder.getCustomerCards() |
Yes | Lists the customer's saved cards. |
tonder.removeCustomerCard(card_id) |
Yes | Removes a saved card. |
tonder.pay() with { type: 'saved_card', card_id } |
Yes | Looks up the saved card and may update it with CVV/Card-on-File data before charging it. |
tonder.pay() with { type: 'card' } |
Only when Card on File is enabled for the business | Saves the new card and creates/updates the Card-on-File subscription before charging it. |
Create the token on your backend using your Tonder secret API key, then return only the short-lived access token to the browser. Never expose your secret key in frontend code.
// Example backend route. Keep TONDER_SECRET_API_KEY only on your server.
app.post('/api/tonder/secure-token', async (_req, res) => {
const response = await fetch('https://stage.tonder.io/api/secure-token/', {
method: 'POST',
headers: {
Authorization: `Token ${process.env.TONDER_SECRET_API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
res.status(502).json({ error: 'Unable to create Tonder secure token' });
return;
}
const { access } = await response.json();
res.json({ secure_token: access });
});Use the matching Tonder API host for your environment:
| SDK environment | Backend token URL |
|---|---|
sandbox or stage |
https://stage.tonder.io/api/secure-token/ |
production |
https://app.tonder.io/api/secure-token/ |
Then pass the value returned by your backend to the SDK:
const { secure_token } = await fetch('/api/tonder/secure-token', {
method: 'POST',
}).then((response) => response.json());
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
session: {
customer: { email: 'ada@example.com' },
secure_token,
},
});<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">Pay</button>
</form>Cap each SDK mount container so the secure iframe does not visually grow before it settles into the input layout:
.card-field {
width: 100%;
max-height: 90px;
}import { createTonder } from '@tonder.io/web-sdk';
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
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' },
});
if (transaction.status === 'Success' || transaction.status === 'Authorized') {
// Show confirmation.
} else if (transaction.status === 'Pending') {
// The customer may need to complete 3DS or an asynchronous payment method.
// Confirm final state with webhooks or getTransaction().
} else {
// Show a recoverable payment message.
console.warn(transaction.decline_code, transaction.decline_reason);
}createTonder(config) creates one SDK instance for one shopper/session. Recreate the SDK if the customer, secure_token, or environment changes.
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
presentation_mode: 'embedded',
session: {
customer: {
email: 'ada@example.com',
first_name: 'Ada',
last_name: 'Lovelace',
phone: '+525500000000',
},
secure_token: await getSecureTokenFromYourBackend(),
},
events: {
presentation: {
on_open: () => console.log('Hosted payment view opened'),
on_close: () => console.log('Shopper closed the hosted payment view'),
},
},
customization: {
card_fields: {
labels: {
card_number: 'Card number',
cvv: 'Security code',
},
placeholders: {
card_number: '4111 1111 1111 1111',
expiration_month: 'MM',
expiration_year: 'YY',
},
error_messages: {
required: 'Complete this field.',
invalid: 'Check this field.',
card_number: 'Enter a valid card number.',
cvv: 'Enter the security code.',
},
},
},
});| Field | Required | Description |
|---|---|---|
api_key |
Yes | Public Tonder key for browser integrations. |
environment |
Yes | 'sandbox', 'stage', or 'production'. |
session.customer |
For pay() and saved-card operations |
Customer identity. Omit for read-only return pages that only call getTransaction(). |
session.secure_token |
For saved-card/Card-on-File operations | Short-lived token minted by your backend using your Tonder secret API key. |
presentation_mode |
No | 'redirect' by default, or 'embedded' for SDK-owned modal presentation. |
events.presentation.on_open |
No | Called when an embedded hosted-payment view opens. |
events.presentation.on_close |
No | Called when the shopper closes a closable embedded hosted-payment view. |
customization.card_fields |
No | Labels, placeholders, styles, and validation-message overrides for secure card fields. |
Configure secure card-field copy and styles through customization.card_fields in createTonder(). All fields are optional; omitted values use the SDK defaults.
| Field | Type | Required | Description |
|---|---|---|---|
labels |
CardLabels |
No | Text shown above each secure field. |
placeholders |
CardPlaceholders |
No | Placeholder text shown inside each secure field. |
styles |
CardStyles |
No | Global and per-field style overrides for secure fields, labels, errors, and card icon. |
error_messages |
CardFieldErrorMessages |
No | Validation-message overrides for empty or invalid fields. |
| Field | Type | Description |
|---|---|---|
cardholder_name |
string |
Label for the cardholder-name field. |
card_number |
string |
Label for the card-number field. |
cvv |
string |
Label for the CVV field. |
expiry_date |
string |
Label for a combined expiry-date field when supported. |
expiration_month |
string |
Label for the expiration-month field. |
expiration_year |
string |
Label for the expiration-year field. |
| Field | Type | Description |
|---|---|---|
cardholder_name |
string |
Placeholder for the cardholder-name field. |
card_number |
string |
Placeholder for the card-number field. |
cvv |
string |
Placeholder for the CVV field. |
expiration_month |
string |
Placeholder for the expiration-month field. |
expiration_year |
string |
Placeholder for the expiration-year field. |
styles.card_form defines defaults for every secure field. Per-field style entries override those defaults only for that field.
| Field | Type | Description |
|---|---|---|
card_form |
FieldStyles |
Default styles applied to every field. |
cardholder_name |
FieldStyles |
Overrides for the cardholder-name field. |
card_number |
FieldStyles |
Overrides for the card-number field. |
cvv |
FieldStyles |
Overrides for the CVV field. |
expiration_month |
FieldStyles |
Overrides for the expiration-month field. |
expiration_year |
FieldStyles |
Overrides for the expiration-year field. |
enable_card_icon |
boolean |
Shows the card-network icon inside the card-number field. Defaults to true. |
FieldStyles accepts these groups:
| Field | Type | Description |
|---|---|---|
input_styles |
CollectInputStyles |
Styles applied to the secure input. |
label_styles |
LabelStyles |
Styles applied to the field label. |
error_styles |
ErrorTextStyles |
Styles applied to validation text. |
CollectInputStyles variants:
| Variant | Description |
|---|---|
base |
Default input style. |
focus |
Style applied while the field is focused. |
complete |
Style applied when the field is complete. |
invalid |
Style applied when the field is invalid. |
empty |
Style applied when the field is empty. |
global |
Global input style overrides supported by the secure renderer. |
cardIcon |
Style overrides for the card-network icon when supported. |
LabelStyles variants:
| Variant | Description |
|---|---|
base |
Default label style. |
global |
Global label style overrides supported by the secure renderer. |
requiredAsterisk |
Style overrides for the required-field asterisk when supported. |
ErrorTextStyles variants:
| Variant | Description |
|---|---|
base |
Default validation-message style. |
global |
Global validation-message style overrides supported by the secure renderer. |
Style values use CSS-in-JS keys supported by the secure card-field renderer, for example font_size, font_family, color, border_color, or letter_spacing.
customization.card_fields.styles styles SDK-rendered content inside the secure iframe, including the secure input, label, validation message, and card icon. Keep merchant layout constraints for the mount containers in CSS, for example .card-field { width: 100%; max-height: 90px; }.
| Field | Type | Description |
|---|---|---|
required |
string |
Generic empty-field message. |
invalid |
string |
Generic invalid-field fallback. |
cardholder_name |
string |
Invalid cardholder-name message. |
card_number |
string |
Invalid card-number message. |
expiration_month |
string |
Invalid expiration-month message. |
expiration_year |
string |
Invalid expiration-year message. |
cvv |
string |
Invalid CVV message. |
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
customization: {
card_fields: {
labels: {
card_number: 'Card number',
cvv: 'Security code',
},
placeholders: {
cardholder_name: 'Ada Lovelace',
card_number: '4111 1111 1111 1111',
expiration_month: 'MM',
expiration_year: 'YY',
},
styles: {
card_form: {
input_styles: {
base: {
color: '#111827',
font_family: 'Inter, sans-serif',
font_size: '16px',
},
focus: { border_color: '#2563eb' },
invalid: { color: '#b91c1c' },
},
label_styles: {
base: { color: '#374151', font_weight: '600' },
},
error_styles: {
base: { color: '#b91c1c' },
},
},
card_number: {
input_styles: {
base: { letter_spacing: '0.03em' },
},
},
enable_card_icon: true,
},
error_messages: {
required: 'Complete this field.',
invalid: 'Check this field.',
card_number: 'Enter a valid card number.',
cvv: 'Enter the security code.',
},
},
},
});Call await tonder.init() before mounting card fields, creating payments, or using saved-card operations. getTransaction(), getPaymentMethods(), and getPaymentMethodBanks() are read-only and can be used without init().
session.customer is optional at createTonder() time. It is required when the SDK creates a payment or manages saved cards.
// Return page / read-only reconciliation.
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
});
const transaction = await tonder.getTransaction('txn_123');Card on File (COF) lets a business save a shopper's card and charge it later through a processor-backed subscription/authorization. Ask the Tonder team whether COF is enabled for your business before building saved-card flows.
When COF is enabled, saved cards may include subscription_id. Cards with subscription_id can be charged directly as saved cards. Cards without subscription_id require CVV collection so the SDK can save/update the card and create the subscription before processing the payment. In both saved-card cases, pay({ payment_method: { type: 'saved_card' } }) still needs session.secure_token because the SDK must read the customer's saved-card record before deciding which path to use.
Because those operations create, list, update, or remove stored card records, they require both:
session.customersession.secure_token
For new-card payments, session.secure_token is only required when the SDK must perform Card-on-File setup as part of the payment flow. Plain one-time new-card payments do not require it.
When a payment requires a hosted step, the SDK uses presentation_mode:
| Mode | Behavior |
|---|---|
redirect |
Browser navigates to the hosted page. Use return_url, getTransaction(), and webhooks to confirm final status. |
embedded |
SDK opens a full-screen modal. Card 3DS waits for a final transaction; APM/SPEI hosted instructions may return Pending immediately. |
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
presentation_mode: 'embedded',
events: {
presentation: {
on_open: () => showLoadingOverlay(false),
on_close: () => console.log('Customer closed the hosted view'),
},
},
});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',
payment_method: { type: 'card' },
});Saved-card operations require both session.customer and session.secure_token. If you are not sure whether your business has Card on File enabled, confirm it with the Tonder team before launching this flow.
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: tonderPublicConfig.environment,
session: {
customer: { email: 'ada@example.com' },
secure_token: await getSecureTokenFromYourBackend(),
},
});
await tonder.init();
const cards = await tonder.getCustomerCards();
const selected_card = cards[0];
// Mount saved-card CVV only when the card cannot be charged through an
// existing Card-on-File subscription. The SDK collects this update context
// automatically during pay().
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 },
});Card enrollment requires both session.customer and session.secure_token. Mint the secure token on your backend before creating the SDK instance.
const card_fields = tonder.create('card_fields');
await card_fields.mount();
const enrollment = await tonder.enrollCard();
// { card_id: 'card_123', subscription_id: 'sub_123' }Use getPaymentMethods() when you want to render the methods enabled for your business. This call is optional: if your checkout already knows which method it wants to offer, pass the method code directly to pay() ({ type: 'spei' }, { type: 'oxxopay' }, etc.).
For bank-backed SafetyPay methods, use getPaymentMethodBanks() and build payment_method.config from the selected bank:
| Field | Value |
|---|---|
country |
bank.country from getPaymentMethodBanks() (for example, Mexico). |
channel |
bank.channel from getPaymentMethodBanks() (WP for cash, OL for transfer). |
bank_ids |
[{ id: bank.code }] using the bank routing code, not the internal bank.id. |
const methods = await tonder.getPaymentMethods();
const banks = await tonder.getPaymentMethodBanks();const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'oxxopay' },
});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, // e.g. 'Mexico'
channel: bank.channel, // 'WP' for cash, 'OL' for transfer
bank_ids: [{ id: bank.code }], // e.g. [{ id: '8186' }]
},
},
});APM/SPEI methods often settle asynchronously. Use webhooks for fulfillment.
Creates an SDK instance.
interface TonderConfig {
api_key: string;
environment: 'sandbox' | 'stage' | 'production';
session?: {
customer?: {
email: string;
first_name?: string;
last_name?: string;
phone?: string;
};
secure_token?: string;
};
presentation_mode?: 'redirect' | 'embedded';
events?: {
presentation?: {
on_open?(): void;
on_close?(): void;
};
};
customization?: TonderCustomization;
}Returns a Tonder SDK instance.
| Code | When |
|---|---|
INIT_ERROR |
config is missing, api_key is missing, or environment is invalid. |
Fetches merchant configuration and prepares the SDK for card fields and payments. Safe to call more than once.
No arguments.
Promise<void>;| Code | When |
|---|---|
INIT_ERROR |
Merchant configuration or initialization fails. |
Creates a secure card-fields component. Call mount() on the returned component to render fields. If options is omitted, the SDK mounts the full new-card form using the default container IDs.
type CardField =
| 'cardholder_name'
| 'card_number'
| 'expiration_month'
| 'expiration_year'
| 'cvv';
interface CardFieldsOptions {
fields?: (CardField | { field: CardField; container_id?: string })[];
card_id?: string;
unmount_context?: 'all' | 'none' | 'current' | 'create' | string;
events?: Partial<
Record<
CardField,
{
on_change?(state: CardFieldState): void;
on_blur?(state: CardFieldState): void;
on_focus?(state: CardFieldState): void;
on_ready?(state: CardFieldState): void;
}
>
>;
}Default container IDs:
| Field | Default container |
|---|---|
cardholder_name |
#collect-cardholder-name |
card_number |
#collect-card-number |
expiration_month |
#collect-expiration-month |
expiration_year |
#collect-expiration-year |
cvv |
#collect-cvv or #collect-cvv-<card_id> for saved cards |
interface CardFieldsComponent {
mount(): Promise<void>;
unmount(): void;
reveal(input: RevealCardFieldsInput): Promise<void>;
}| Code | When |
|---|---|
INVALID_COMPONENT_TYPE |
The first argument is not 'card_fields'. |
Mounts secure card fields into the configured containers.
Each container should cap its layout height before mount() runs, for example .card-field { width: 100%; max-height: 90px; }, to avoid a visual jump while the secure iframe initializes.
No arguments. Containers are configured in tonder.create('card_fields', options?). If no options are provided, the SDK uses the default full-card containers.
Promise<void>;| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed. |
SECURE_FIELDS_LOAD_ERROR |
Secure card fields could not load in the browser. |
VAULT_TOKEN_ERROR |
Tonder could not prepare the secure card fields session. |
INVALID_VAULT_TOKEN |
Tonder returned an invalid secure card fields session. |
MOUNT_COLLECT_ERROR |
A configured field cannot be mounted. |
Unmounts this component's secure card fields.
No arguments.
voidReveals display-safe saved-card values into merchant containers. CVV cannot be revealed.
type RevealableCardField =
| 'cardholder_name'
| 'card_number'
| 'expiration_month'
| 'expiration_year';
interface RevealCardFieldsInput {
fields: (
| RevealableCardField
| {
field: RevealableCardField;
container_id?: string;
alt_text?: string;
label?: string;
styles?: CardFieldsCustomization['styles'];
}
)[];
styles?: CardFieldsCustomization['styles'];
}Promise<void>;| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed or no card tokens are available to reveal. |
SECURE_FIELDS_LOAD_ERROR |
Secure card fields could not load in the browser. |
VAULT_TOKEN_ERROR |
Tonder could not prepare the secure card fields session. |
INVALID_VAULT_TOKEN |
Tonder returned an invalid secure card fields session. |
Creates a payment.
For { type: 'saved_card', card_id }, tonder.pay() requires session.secure_token because the SDK must look up the saved card and may collect CVV/update Card-on-File data before charging it. For { type: 'card' }, session.secure_token is only required when Card on File is enabled for the business and the SDK must save the new card before processing the payment.
interface PayInput {
amount: number;
currency?: string;
return_url: string;
payment_method:
| { type: 'card' }
| { type: 'saved_card'; card_id: string }
| { type: string; config?: Record<string, unknown> };
metadata?: Record<string, unknown>;
billing_address?: {
street?: string;
street2?: string;
state?: string;
country?: string;
zip_code?: string;
};
client_reference: string;
idempotency_key?: string;
}| Field | Required | Description |
|---|---|---|
amount |
Yes | Payment amount. Must be greater than 0. |
currency |
No | Currency code. Defaults to MXN when omitted. |
return_url |
Yes | URL used after hosted authentication or redirect completion. |
payment_method |
Yes | Payment method to charge: new card, saved card, or an enabled alternative payment method. |
client_reference |
Yes | Merchant order/reference shown in dashboards, exports, webhooks, transaction records, and transaction reports. |
idempotency_key |
No | Recommended stable key for the same payment attempt so retries do not create duplicate charges. |
metadata |
No | Non-sensitive merchant context for reconciliation and reports. |
billing_address |
No | Customer billing address. All sub-fields (street, street2, state, country, zip_code) are optional. |
Examples:
await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'card' },
});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: 'card_123' },
});await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'spei' },
});client_reference is the required merchant/business reference that remains in the payment payload and appears in dashboards, exports, webhooks, transaction records, and transaction reports as the customer order reference.
idempotency_key is important for retry-safe checkout flows: keep it stable for the same payment attempt so retries do not create duplicate charges. Do not reuse client_reference as the idempotency key.
Use metadata for non-sensitive merchant context that helps reconciliation and reports. You can send any JSON-safe fields your commerce system needs. These metadata keys have reporting meaning when present:
| Metadata key | Report usage |
|---|---|
operation_date |
Business operation date/time for reporting and reconciliation. |
customer_email |
Customer email shown in transaction reports; falls back to the customer email when omitted. |
customer_id |
Merchant customer identifier for report filtering and reconciliation. |
business_user |
Internal user, POS terminal, cashier, or automation that initiated the payment. |
await tonder.pay({
amount: 150,
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
idempotency_key: 'checkout-attempt-1001-1',
metadata: {
customer_email: 'ada@example.com',
customer_id: 'cus_123',
business_user: 'pos-terminal-4',
// ... other fields
},
payment_method: { type: 'card' },
});Returns Promise<RawTransaction>.
{
"id": "txn_123",
"operation_type": "payment",
"status": "Authorized",
"amount": 150,
"currency": "MXN",
"client_reference": "order_1001",
"metadata": { "cart_id": "cart_789" },
"created_at": "2026-07-06T18:00:00Z"
}A transaction that needs 3DS or hosted instructions can include next_action:
{
"id": "txn_123",
"operation_type": "payment",
"status": "Pending",
"amount": 150,
"currency": "MXN",
"next_action": {
"redirect_to_url": {
"url": "https://hosted-payment.example/checkout/..."
}
}
}APM/SPEI responses may include settlement fields:
{
"id": "txn_123",
"operation_type": "payment",
"status": "Pending",
"amount": 150,
"currency": "MXN",
"clabe": "646180123400000001",
"bank_name": "STP",
"payment_instructions": { "reference": "1234567890" },
"voucher_pdf": "https://..."
}| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed. |
MISSING_CUSTOMER |
session.customer was not configured. |
INVALID_PAYMENT_REQUEST |
amount, return_url, or payment_method is invalid. |
INVALID_APM_CONFIG |
safetypayCash or safetypayTransfer is missing config.country, config.channel, or config.bank_ids. |
MOUNT_COLLECT_ERROR |
Card fields cannot be collected. |
PAYMENT_PROCESS_ERROR |
The payment request fails. |
FETCH_TRANSACTION_ERROR |
Hosted/3DS resolution cannot retrieve the transaction. |
POLL_TIMEOUT_ERROR |
Embedded card 3DS signaled completion, but reconciliation did not reach a final status in time. |
REQUEST_ABORTED |
The embedded hosted-payment wait was canceled. |
SAVE_CARD_ERROR, REMOVE_CARD_ERROR, CARD_ON_FILE_DECLINED |
Card-on-file setup or rollback fails. |
Reads the current transaction state. Useful on return_url pages and admin/reconciliation views. Does not require session.customer or init().
tonder.getTransaction(id: string): Promise<RawTransaction>Same RawTransaction shape as pay().
| Code | When |
|---|---|
FETCH_TRANSACTION_ERROR |
The transaction cannot be retrieved. |
REQUEST_ABORTED |
The browser request was canceled. |
Saves the currently mounted new card for session.customer. Requires session.secure_token because card enrollment is a card CRUD/Card-on-File operation.
No arguments. Requires a mounted new-card card_fields component.
interface EnrollResult {
card_id: string;
subscription_id?: string;
}| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed. |
MISSING_CUSTOMER |
session.customer was not configured. |
SECURE_TOKEN_REQUIRED |
session.secure_token was not configured. |
MOUNT_COLLECT_ERROR |
Card fields cannot be collected. |
CUSTOMER_OPERATION_ERROR |
Customer registration/fetch fails. |
SAVE_CARD_ERROR |
Card save fails. |
CARD_ON_FILE_DECLINED |
Card-on-file enrollment is declined. |
ACQUIRER_LOAD_ERROR |
Card-on-file processor library cannot load. |
Lists saved cards for session.customer. subscription_id is returned only when Card-on-File is enabled for the business. When it is null, mount the saved-card CVV field before calling pay() with that card.
No arguments.
interface Card {
card_id: string;
card_number: string; // masked
expiration_month: string;
expiration_year: string;
card_scheme: string;
subscription_id: string | null;
}Example:
[
{
"card_id": "card_123",
"card_number": "XXXX-XXXX-XXXX-4242",
"expiration_month": "12",
"expiration_year": "29",
"card_scheme": "visa",
"subscription_id": "sub_123"
}
]| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed. |
MISSING_CUSTOMER |
session.customer was not configured. |
SECURE_TOKEN_REQUIRED |
session.secure_token was not configured. |
CUSTOMER_OPERATION_ERROR |
Customer registration/fetch fails. |
FETCH_CARDS_ERROR |
Saved cards cannot be retrieved. |
Removes a saved card for session.customer.
tonder.removeCustomerCard(card_id: string): Promise<void>Promise<void>;| Code | When |
|---|---|
NOT_INITIALIZED |
tonder.init() has not completed. |
MISSING_CUSTOMER |
session.customer was not configured. |
SECURE_TOKEN_REQUIRED |
session.secure_token was not configured. |
CUSTOMER_OPERATION_ERROR |
Customer registration/fetch fails. |
REMOVE_CARD_ERROR |
Saved card cannot be removed. |
Lists active payment methods configured for your business. Can be called before init().
This method is for discovery/rendering only. It is not required before pay(): you may pass a known enabled method code directly, such as payment_method: { type: 'spei' } or payment_method: { type: 'oxxopay' }.
No arguments.
interface PaymentMethodInfo {
id: number;
payment_method: string;
label: string;
logo: string;
category: string;
}Example:
[
{
"id": 7,
"payment_method": "oxxopay",
"label": "Oxxo Pay",
"logo": "https://...",
"category": "cash"
}
]| Code | When |
|---|---|
FETCH_PAYMENT_METHODS_ERROR |
Payment methods cannot be retrieved. |
Lists SafetyPay bank options grouped by channel. Can be called before init().
No arguments.
interface PaymentMethodBank {
id: number;
name: string;
code: string;
country: string;
channel: 'WP' | 'OL';
logo?: string;
}
interface PaymentMethodBanks {
cash: PaymentMethodBank[];
transfer: PaymentMethodBank[];
}Example:
{
"cash": [
{
"id": 47,
"name": "Banco Azteca",
"code": "8186",
"country": "Mexico",
"channel": "WP",
"logo": "https://..."
}
],
"transfer": []
}| Code | When |
|---|---|
FETCH_PAYMENT_METHOD_BANKS_ERROR |
Bank options cannot be retrieved. |
Useful exports:
import type {
TonderConfig,
PayInput,
RawTransaction,
Customer,
Card,
EnrollResult,
PaymentMethodInfo,
PaymentMethodBank,
PaymentMethodBanks,
CardFieldsOptions,
CardFieldsComponent,
TonderEvents,
PresentationEvents,
} from '@tonder.io/web-sdk';If you load the SDK runtime from the CDN in a TypeScript app, you can still install @tonder.io/web-sdk as a devDependency for types only. See CDN with TypeScript types.
pay() and getTransaction() return transaction fields in snake_case, matching Tonder API and webhook payloads.
interface RawTransaction {
id: string;
operation_type: string;
status: string;
amount: number;
currency: string;
client_reference?: string;
metadata?: Record<string, unknown>;
provider?: string;
created_at?: string;
status_code?: number;
next_action?: {
redirect_to_url?: {
url: string;
verify_transaction_status_url?: string;
};
};
decline_code?: string;
decline_reason?: string;
payment_instructions?: Record<string, unknown>;
voucher_pdf?: string;
clabe?: string;
bank_name?: string;
[key: string]: unknown;
}SDK failures are thrown as AppError. Payment declines are returned as transactions; read transaction.status and use the Payment statuses table to decide the next action.
import { AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';
try {
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'card' },
});
} catch (error) {
if (error instanceof AppError) {
console.error(error.code, error.status_code, error.details.system_error);
if (error.code === ErrorKeyEnum.MISSING_CUSTOMER) {
// Recreate the SDK with session.customer.
}
} else {
throw error;
}
}Example error shape:
{
"name": "TonderError",
"status": "error",
"code": "INVALID_PAYMENT_REQUEST",
"status_code": 500,
"details": {
"code": "INVALID_PAYMENT_REQUEST",
"status_code": 500,
"system_error": "input.amount must be greater than 0."
}
}Common error codes:
Use error.code for branching. Do not parse error.message; messages are for display/logging and may change.
| Code | When it happens | Returned by | How to fix |
|---|---|---|---|
INIT_ERROR |
SDK initialization failed after the instance was created. | tonder.init() |
Check the publishable api_key, environment, and network access to Tonder services. |
FETCH_BUSINESS_ERROR |
Merchant/business configuration could not be loaded. | tonder.init(), operations that require initialization |
Verify the publishable key and environment. |
NOT_INITIALIZED |
A method needs initialized SDK state but init() has not completed. |
card_fields.mount(), card_fields.reveal(), tonder.pay(), tonder.enrollCard(), saved-card methods |
Call await tonder.init() before the operation. Read-only methods such as getTransaction() and payment-method catalog methods do not need init(). |
INVALID_COMPONENT_TYPE |
The requested UI component is not supported. | tonder.create() |
Use tonder.create('card_fields', options). |
SECURE_FIELDS_LOAD_ERROR |
The browser could not load the secure card-fields library. | card_fields.mount(), card_fields.reveal() |
Check CSP, ad blockers, network access, and the page environment. |
ACQUIRER_LOAD_ERROR |
The Card-on-File acquirer library could not load. | tonder.enrollCard(), tonder.pay() when COF is needed |
Check network/CSP and retry. |
| Code | When it happens | Returned by | How to fix |
|---|---|---|---|
MISSING_CUSTOMER |
session.customer is required but was not provided. |
tonder.pay(), tonder.enrollCard(), tonder.getCustomerCards(), tonder.removeCustomerCard() |
Create the SDK with session.customer.email and optional customer fields. |
SECURE_TOKEN_REQUIRED |
Saved-card/Card-on-File operations require session.secure_token. |
tonder.enrollCard(), tonder.getCustomerCards(), tonder.removeCustomerCard(), tonder.pay() with { type: 'saved_card' }, tonder.pay() with { type: 'card' } when COF is enabled |
Mint a secure token on your backend and pass it in createTonder({ session: { secure_token } }). |
CUSTOMER_OPERATION_ERROR |
Customer registration/fetch failed. | Saved-card operations and card enrollment | Verify customer data and backend availability. |
FETCH_CARDS_ERROR |
Saved cards could not be retrieved. | tonder.getCustomerCards(), saved-card pay() lookup |
Verify session.customer, session.secure_token, and customer ownership. |
SAVE_CARD_ERROR |
A new or existing card could not be saved. | tonder.enrollCard(), tonder.pay() when a card must be saved for COF |
Ask the shopper to verify card data or retry; inspect error.details for backend context. |
REMOVE_CARD_ERROR |
A saved card could not be removed. | tonder.removeCustomerCard(), rollback after failed auto-enrollment |
Retry the removal or reconcile from your backend/admin tools. |
CARD_ON_FILE_DECLINED |
Card-on-File enrollment/authorization was declined by the processor. | tonder.enrollCard(), COF/saved-card tonder.pay() flows |
Ask for another card or corrected card details. |
| Code | When it happens | Returned by | How to fix |
|---|---|---|---|
INVALID_PAYMENT_REQUEST |
Required payment fields are missing or invalid (amount, return_url, client_reference, payment_method, saved-card card_id, etc.). |
tonder.pay() |
Validate the request before calling pay(). |
INVALID_PAYMENT_REQUEST_CARD_PM |
A card payment path received a non-card method. | tonder.pay() |
Use { type: 'card' } for new-card payments or a supported APM code for alternative methods. |
INVALID_APM_CONFIG |
safetypayCash or safetypayTransfer is missing required config. |
tonder.pay() |
Pass payment_method.config.country, payment_method.config.channel, and payment_method.config.bank_ids using the selected PaymentMethodBank. |
MOUNT_COLLECT_ERROR |
Secure fields could not mount or collect valid card data. | card_fields.mount(), tonder.pay(), tonder.enrollCard() |
Ensure all field containers exist and the shopper completed valid card fields. |
PAYMENT_PROCESS_ERROR |
Tonder could not create/process the payment or the transport failed. Declined payments returned by Tonder are not thrown; they are returned as transactions. | tonder.pay() |
Inspect error.details and reconcile with your backend logs. Retry only when safe/idempotent. |
FETCH_TRANSACTION_ERROR |
Transaction lookup failed. | tonder.getTransaction(), embedded 3DS reconciliation in tonder.pay() |
Verify the transaction id and retry from backend/webhook records. |
REQUEST_ABORTED |
A request or embedded hosted-payment wait was canceled. | tonder.pay(), tonder.getTransaction() |
Treat as an interrupted client flow and reconcile from backend/webhooks. |
REQUEST_FAILED |
A low-level network/HTTP request failed before it could be mapped to a more specific operation. | Browser/network-dependent operations | Check connectivity, CORS/CSP, and API availability. |
POLL_TIMEOUT_ERROR |
Embedded card 3DS signaled completion, but reconciliation did not reach a final status in time. | tonder.pay() for embedded card 3DS |
Do not fulfill from the client result alone; reconcile with getTransaction() or webhooks. |
| Code | When it happens | Returned by | How to fix |
|---|---|---|---|
FETCH_PAYMENT_METHODS_ERROR |
Active payment methods could not be retrieved. | tonder.getPaymentMethods() |
Retry or offer known method codes directly through pay(). getPaymentMethods() is optional for checkout rendering. |
FETCH_PAYMENT_METHOD_BANKS_ERROR |
SafetyPay bank options could not be retrieved. | tonder.getPaymentMethodBanks() |
Retry later or hide SafetyPay bank-backed options until banks are available. |
| Code | When it happens | Returned by | How to fix |
|---|---|---|---|
VAULT_TOKEN_ERROR |
The SDK could not prepare a secure card-fields session. | card_fields.mount(), card_fields.reveal() |
Verify merchant vault configuration and retry. |
INVALID_VAULT_TOKEN |
Tonder returned an invalid secure card-fields session response. | card_fields.mount(), card_fields.reveal() |
Retry and contact Tonder if it persists. |
These codes are part of the stable enum for compatibility, but they are not expected in normal Web SDK integrations unless a lower-level adapter or legacy path surfaces them.
| Code | Meaning |
|---|---|
CREATE_ERROR |
SDK creation failed before a usable instance was returned. |
INVALID_TYPE |
Legacy/compat SDK type validation failed. |
STATE_ERROR |
Internal SDK state update failed. |
INVALID_CONFIG |
Required configuration is missing or malformed. |
MERCHANT_CREDENTIAL_REQUIRED |
Merchant credential is missing for a lower-level operation. |
ENVIRONMENT_REQUIRED |
Environment was not provided. |
CUSTOMER_AUTH_TOKEN_NOT_VALID |
Customer auth token was rejected by a lower-level saved-card request. |
INVALID_CARD_DATA |
Card data failed lower-level validation. |
SAVE_CARD_PROCESS_ERROR |
Lower-level save-card processing failed. |
SECURE_TOKEN_INVALID |
Secure token was rejected by a lower-level operation. |
INVALID_EMAIL |
Customer email failed validation. |
THREEDS_REDIRECTION_ERROR |
3DS redirection failed in a lower-level hosted flow. |
UNKNOWN_ERROR |
Unexpected SDK error fallback. |
Read payment state from transaction.status.
| Status | Meaning | What to do |
|---|---|---|
Success |
Payment completed. | Confirm the order. |
Authorized |
Payment was authorized by the processor path. | Continue according to your Tonder setup and reconcile with webhooks. |
Pending |
Payment is not final yet. Common for redirect 3DS and asynchronous APM/SPEI flows. | Wait for webhook confirmation or read later with getTransaction(). |
Processing |
Payment is still being processed by the provider. | Do not fulfill yet; wait for webhook or query again later. |
Declined |
Issuer/processor declined the payment. | Show a recoverable payment message. |
Failed |
Payment failed. | Show a recoverable payment message or ask for another method. |
Cancelled |
Payment was cancelled or voided. | Do not fulfill; let the shopper start a new payment if needed. |
Expired |
Payment was not completed in time. | Ask the customer to start a new payment. |
Webhooks are server-to-server notifications from Tonder to your backend. Use them as the source of truth for post-payment events and fulfillment, especially when the shopper leaves the browser flow or the payment completes asynchronously.
Use webhooks when:
- A payment can complete after the shopper leaves your page.
- You use asynchronous methods such as SPEI, OXXO, SafetyPay, or Mercado Pago.
- You need reliable order fulfillment, inventory release, receipts, or ledger updates.
- You need to reconcile
Pendingtransactions after redirect/hosted-payment flows.
Tonder webhooks use a flat payload: fields are at the top level, not wrapped in a nested data object. Common payment fields include:
| Field | Type | Description |
|---|---|---|
id |
string | Unique webhook event identifier. |
operation_type |
string | Operation type, usually payment for this SDK. |
amount |
string | Transaction amount as sent by the webhook event. |
currency |
string | ISO currency code, for example MXN. |
client_reference |
string | Your own order/reference identifier. |
status |
string | Current transaction status. See Payment statuses. |
provider |
string | Payment provider/acquirer that processed the transaction. |
transaction_id |
string | Tonder transaction identifier. |
payment_method_type |
string | Payment method used, for example CARD, SPEI, or OXXO. |
created |
string | ISO timestamp for the event. |
metadata |
object | Metadata you passed when creating the payment. |
event_type |
string | Event name, for example payment_Success or payment_Pending. |
action |
string | Event action, for example MODIFY. |
Example payment_Success event:
{
"id": "fc38522e-3e5d-45b8-ba6a-ece72caee71f",
"operation_type": "payment",
"amount": "70",
"currency": "MXN",
"client_reference": "order_1001",
"status": "Success",
"provider": "tonder",
"transaction_id": "e9340a04-6d68-4afc-86c5-79f8b7c87de4",
"payment_method_type": "SPEI",
"created": "2026-05-21T19:15:32.029134Z",
"metadata": {
"cart_id": "cart_789"
},
"event_type": "payment_Success",
"action": "MODIFY"
}Webhook endpoint checklist:
- Use a publicly reachable HTTPS URL.
- Verify the request comes from Tonder according to your account configuration.
- Respond within 30 seconds.
- Return any
2xxstatus to acknowledge receipt. - Make processing idempotent by storing processed event IDs.
For setup, retry behavior, and delivery details, see How webhooks work.
For browser <script> usage, load the SDK from the environment CDN:
| Environment | CDN URL |
|---|---|
| Stage | https://zplit-stage.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js |
| Production | 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>The CDN build exposes the SDK at window.Tonder. If a React, Angular, or TypeScript app loads the runtime from the CDN, TypeScript does not automatically know the global SDK shape.
You can install the npm package as a development dependency only for types while still using the CDN runtime:
npm install -D @tonder.io/web-sdkimport type * as TonderWebSdk from '@tonder.io/web-sdk';
declare global {
interface Window {
Tonder: typeof TonderWebSdk;
}
}Keep the CDN script in your HTML entry point and use the browser global at runtime:
const { createTonder } = window.Tonder;Do not import runtime code from @tonder.io/web-sdk when using the CDN setup. The package is installed only as a devDependency for TypeScript types.