Web
Integrate the Web SDK: secure card fields, saved cards and alternative payment methods.
Tonder's Web SDK (@tonder.io/web-sdk) is a
browser TypeScript SDK for accepting payments: secure card fields, new-card and saved-card
payments, hosted/3DS presentation, payment-method discovery, transaction lookup, and
webhook-friendly transaction responses.
Want an AI agent to do the integration for you? Install the Tonder Web SDK plugin in Claude Code, Claude Desktop, or Codex.
Before you start
You need:
- Your 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_token, if you'll use saved cards / Card-on-File. - A webhook endpoint for reliable payment fulfillment.
Install
npm install @tonder.io/web-sdkimport { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';No bundler? Load the browser global build 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>In a TypeScript app that uses the CDN, you can install @tonder.io/web-sdk as a devDependency
for types only (npm install -D @tonder.io/web-sdk + import type), keeping the runtime on
the CDN. Do not import runtime code from the package in that setup.
Quick start: card payment
Add containers for the card fields
<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 container's height so the secure iframe does not visually grow before it settles into the input layout:
.card-field {
width: 100%;
max-height: 90px;
}Initialize, mount, and pay
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' },
});Handle the result
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);
}Declines are not thrown as errors — they're returned as transactions: read
transaction.status. SDK failures are thrown as AppError (see
Errors).
Web SDK amounts are decimal units (150 = MXN 150.00). See
Money, currencies, and amounts.
Configuration
createTonder(config) creates one SDK instance for one shopper/session. Recreate the SDK if the
customer, secure_token, or environment changes.
| 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 operations | Short-lived token minted by your backend. |
presentation_mode | No | 'redirect' by default, or 'embedded' for SDK-owned modal presentation. |
events.presentation | No | on_open / on_close callbacks for the embedded hosted view. |
customization.card_fields | No | Labels, placeholders, styles, and validation-message overrides for the secure fields. |
The full customization tables (per-field styles, error messages, card icon) are in the Web SDK Reference.
Presentation mode
When a payment requires a hosted step (3DS, APM instructions), the SDK uses
presentation_mode:
| Mode | Behavior |
|---|---|
redirect | The browser navigates to the hosted page. Use return_url, getTransaction(), and webhooks to confirm final status. |
embedded | The SDK opens a full-screen modal. Card 3DS waits for a final transaction; APM/SPEI hosted instructions may return Pending immediately. |
Saved cards (secure_token)
Saved-card operations (getCustomerCards(), enrollCard(), removeCustomerCard(), paying with
saved_card) require session.customer and session.secure_token. Mint the token by
calling /api/secure-token/ from your backend with your secret key:
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;
// Pass it to the frontend as 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;
});The generated secure_token is valid for 1 hour. Use it within that window; if you cache or
reuse it after that, mint a new one.
Pay with a saved card
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];
// Mount saved-card CVV only when the card cannot be charged through an
// existing Card-on-File subscription.
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 },
});Save a new card
const card_fields = tonder.create('card_fields');
await card_fields.mount();
const enrollment = await tonder.enrollCard();
// { card_id: 'card_123', subscription_id: 'sub_123' }API reference
Authorization
SecretKeyAuth Tu SECRET key con prefijo Token , p. ej. Token <SECRET_KEY> — distinta de la API key
In: header
Response Body
application/json
curl -X POST "https://example.com/secure-token/"{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzI3NzI3MTM3LCJpYXQiOjE3Mjc3MjM1MzcsImp0aSI6IjFjZTBkZmExODgwNzQzNGI4MDk2MzdlNTliNmM1NWMzIiwidXNlcl9pZCI6NDYxfQ.DFGNJr7JT6z3cp976PDBT57uX7LaYJLYBsdK8kaSAOI"
}Alternative payment methods
If your checkout already knows which method to offer, pass the method code directly to 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' },
});Use getPaymentMethods() (optional) to render the methods enabled for your business, and
getPaymentMethodBanks() for bank-backed SafetyPay methods:
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' cash, 'OL' transfer
bank_ids: [{ id: bank.code }], // e.g. [{ id: '8186' }]
},
},
});APM/SPEI methods often settle asynchronously. Use webhooks for fulfillment.
Demos
Try every flow in the SDK demos portal:
| Flow | Demo |
|---|---|
| Card payment | web/card-payment |
| Card enrollment | web/enroll-card |
| Saved cards | web/saved-cards |
| Payment methods | web/payment-methods |
| SafetyPay banks | web/safetypay-banks |
The legacy demos (Web SDK Lite and Inline) use previous SDK versions and are kept for reference only — use the demos above for new integrations. If you are still on the previous SDK, follow the migration guide.
Reconciliation
client_referenceis required: it's your order reference and appears in dashboards, exports, webhooks, and transaction reports.- Use a stable
idempotency_keyper checkout attempt so retries don't create duplicate charges. Do not reuseclient_referenceas the idempotency key. - Web SDK webhooks use the flat payload (top-level fields) with
event_type: payment_Success/payment_Pending. See the events catalog.
