SDKs

Migrating from Direct API

Move some or all of your checkout to the Web SDK without changing your backend: same /process/, same webhooks, same reconciliation.

For merchants already charging through Tonder Direct API server-to-server, who want to move some or all of their checkout to the browser SDK.

The one thing to know first

Your backend contract does not change. The SDK posts to the same /api/v1/process/ with the same body shape, returns the same transaction, and fires the same webhooks. Your reconciliation, your client_reference correlation, your webhook handler, and your GET /api/v1/transactions/{id}/ polling all keep working untouched.

What changes is only this:

TodayWith the SDK
You call the vault to tokenize card dataThe SDK's secure fields tokenize it — you never see the card
You build the /process/ body and POST itpay() builds and posts it
You handle the 3DS redirect yourselfThe SDK presents it, redirect or embedded

Everything after /process/ responds is unchanged.

Pick your path

Two migrations live in this guide. They share nothing but the setup step, so read only yours.

If you want toGo toServer changes
Ship Apple Pay first, without touching your current checkout yetPath ANone
Move cards and APMs into the browser, and add Apple PayPath BOnly if you use saved cards

They are sequential, not alternatives. Path A puts the SDK on your page without changing what you have; Path B then moves the rest of the checkout into it, one flow at a time. See Where Path A leads.

Setup — both paths

Load the SDK (client-side)

Both options work in every framework, React and Next.js included.

Updates reach you
CDN <script> — the SDK arrives as window.TonderAutomatically; the URL tracks a major-version channel (/web-sdk/v1/)
npmnpm install @tonder.io/web-sdkWhen you bump the version and deploy

A TypeScript app can install the npm package as a devDependency for types only and keep the CDN runtime.

Install and CDN snippets: Install.

Create the instance (client-side)

You already hold a secret API key on your server for Direct API. The SDK needs your public key instead, and it goes in browser code. They are different keys; do not reuse the secret one.

const tonder = createTonder({
  api_key: tonderPublicConfig.api_key,
  environment: 'stage', // switch to 'production' when you go live
  session: { customer: { email: 'ada@example.com' } },
});
await tonder.init();

session.customer carries the same identity you send today as customer in the /process/ body. Full configuration reference: createTonder(config).

Path A: start with Apple Pay

The smallest possible first release: your tokenization, your /process/ calls and your reconciliation stay exactly where they are, and the SDK renders one button whose charge lands on the endpoint you already read from.

The full button integration — enablement and domain registration, the availability check, the container, the events.payment callbacks — is in the Apple Pay guide. What matters from the migration's point of view:

Before — you control the submit

const token = await tokenize(cardData);
const tx = await fetch('/your-backend/charge', { method: 'POST', body: /* … */ });
handleResult(tx);

After — the SDK controls the tap, you receive the result

Apple requires the payment sheet to open in the same tick as the tap, so the SDK owns the click, and the result comes back on the events.payment callbacks instead of a returned promise. Those callbacks are not an Apple Pay mechanism — they fire for every method the SDK charges. When you later move cards and APMs across, pay() returns a promise and fires the same callbacks, so one set of handlers keeps covering everything. You can see the real button working in the Apple Pay demo (open it in Safari).

Keep your reconciliation as it is (server-side)

Nothing to do. The Apple Pay charge lands on /api/v1/process/ like your card charges, produces a transaction with the same shape, and fires the same webhook. Your existing handler already covers it. Send client_reference in the button's payment data exactly as you do today and your correlation keeps working.

pay({ payment_method: { type: 'apple_pay' } }) is rejected on purpose — Apple's gesture requirement is why. Use the button component, as shown in the Apple Pay guide.

Where Path A leads

The SDK is now loaded and initialized on your page, so the remaining steps are smaller than the one you just did.

  1. Card collection — raw PAN and CVV stop touching your JavaScript, which removes your code from that part of PCI scope, and your vault tokenization call disappears. Path B, step 1.
  2. APMs — same method codes, one call instead of a request you assemble. Path B, step 3.

Each is a separate release. There is no cutover.

Path B: move your checkout to the SDK

1. Replace tokenization with secure fields (client-side)

This is the step that removes code rather than adding it. Today you collect card data and call the vault yourself; that disappears.

Before

const tokens = await vault.tokenize({
  card_number, cvv, expiration_month, expiration_year, cardholder_name,
});

After — your <input>s become empty containers, and the SDK mounts a secure iframe into each one

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

Those are the default ids; every configured field needs its container present before mount() runs, or the call rejects with MOUNT_COLLECT_ERROR. Give them a max-height in your own CSS so the iframe does not grow before it settles.

Two different places configure these, which is worth getting right the first time:

What you wantWhere it goes
Custom container ids, mounting a subset of fields, per-field eventstonder.create('card_fields', options) — see the reference
Labels, placeholders, styles, validation messagescustomization.card_fields on createTonder()

This is the PCI-relevant change. Raw PAN and CVV stop touching your JavaScript entirely — they go straight from the shopper into Tonder's secure iframe.

2. Replace the /process/ POST with pay() (client-side)

The fields you send are the same ones you send today. Only the caller changes.

Before — your server builds the envelope

{
  "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": "<tokenized>" },
  "return_url": "https://merchant.example.com/return"
}

After — the browser does, from the same values

const transaction = await tonder.pay({
  amount: 250,
  currency: 'MXN',
  client_reference: 'ORD-001',
  return_url: 'https://merchant.example.com/return',
  payment_method: { type: 'card' },
});

Three differences worth noting, all of them simplifications:

  • operation_type is gone. The SDK only creates payments; refunds and withdrawals stay on your server.
  • customer moved to createTonder(). It belongs to the session, not to each charge.
  • The card fields are gone from the body. The SDK collects them from the mounted fields.

Field-by-field reference, including idempotency_key and metadata: tonder.pay(input).

3. Map your APM calls (client-side)

Same call, different payment_method.type. Your APM codes carry over unchanged.

Today, in /process/With the SDK
"type": "SPEI"payment_method: { type: 'spei' }
"type": "oxxopay"payment_method: { type: 'oxxopay' }
"type": "safetypaycash" + apm_configpayment_method: { type: 'safetypaycash', config: { … } }

SafetyPay still needs country, channel, and bank_ids; the SDK can list the banks for you with getPaymentMethodBanks() instead of you hardcoding them. See Alternative payment methods.

APMs return Pending and settle later, exactly as they do today. Your webhook handling does not change.

4. Optional: move 3DS presentation to the SDK (client-side)

If you currently redirect the shopper to the hosted page yourself, you can hand that to the SDK and choose how it appears:

  • presentation_mode: 'redirect' — the browser navigates, as it does today. Your return_url still lands where it does now.
  • presentation_mode: 'embedded' — the SDK opens a modal and the shopper never leaves your page.

See Presentation mode.

5. Optional: saved cards (server-side + client-side)

Only if you want stored cards. This is the one part of Path B that needs a server change: saved-card operations require a short-lived secure_token minted by your backend with your existing Tonder secret key.

One trap. With Card on File enabled, even a one-time card payment needs the token, because the SDK stores the card as part of the charge. It is an account setting, so the same code works for one business and throws SECURE_TOKEN_REQUIRED for another.

The endpoint to build, and which operations need the token: Saved cards (secure_token).

6. Add Apple Pay (client-side)

Follow the Apple Pay guide from step 1. It is the same work whether or not you migrated the rest.

Reconciliation — do not change this

The most common mistake when moving checkout into the browser is starting to trust the browser.

You already fulfil from webhooks, because server-to-server left you no other option. Keep doing exactly that. The SDK returns a transaction so you can update the screen, not so you can release goods — a browser can be closed or lose signal, and neither changes what happened to the money.

  • client_reference — keep sending it, keep correlating on it
  • idempotency_key — keep sending it, so a retried charge cannot become two
  • Webhooks — same payload, same handler, no wrapper. See How webhooks work
  • getTransaction() — the browser-side equivalent of your GET /api/v1/transactions/{id}/, for return pages and one-off checks

Test the migration

Run these in stage before switching production traffic.

CheckWhat proves it worked
A card paymentSame transaction shape in your existing handler as before the migration
A declined cardArrives as a transaction with a declined status, not as a thrown error
A 3DS cardReturns to your return_url, or resolves in the modal if embedded
An APMReturns Pending, then settles by webhook as it does today
Your webhook handlerUntouched code still processes SDK-created payments
Apple Pay, if adoptedSheet opens on a real device — the iOS Simulator cannot test web Apple Pay
Apple Pay declineArrives on on_completed with a declined status, not on on_error

That last row is the one that surprises people: on_completed means the charge reached a final answer, not that the answer was yes.

What stays on your server

The SDK does not replace these. They remain Direct API calls:

  • Refunds — operation_type: "refund"
  • Withdrawals — operation_type: "withdrawal"
  • Any charge you create without a browser present

Next steps

Was this page helpful?

On this page