SDKs

Migrating from the legacy SDK

From tonder-web-sdk v2 (InlineCheckout / LiteInlineCheckout) to @tonder.io/web-sdk, including the webhook change.

For merchants on tonder-web-sdk v2 — InlineCheckout or LiteInlineCheckout — moving to @tonder.io/web-sdk.

Read this before you plan the work

This is not only a front-end change. Your webhook handler has to change too, and that is the part merchants underestimate when they plan the work.

The new SDK charges through a different Tonder payment API than the legacy one did, so the events your server receives have a different payload. It is not a rename of a few fields: some fields disappear, some are new, and if you accept APMs your two current handlers collapse into one. Budget backend time for it — see Webhooks.

Everything else is a feature-for-feature move. Saved cards, Card on File, enrollment, CVV re-collection, APMs, SafetyPay, 3DS and secure fields all exist in the new SDK.

Which class did you import?

You usedYour migration isGo to
LiteInlineCheckoutMostly method renames — you already own your UIPath A
InlineCheckoutYou build the checkout UI the old SDK used to drawPath B

Both paths then share Webhooks, which is the same work either way.

Setup — both paths

Load the SDK (client-side)

The legacy SDK shipped as an npm package and as a script tag. So does this one, and you are not tied to the one you used before — 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

If you were on npm, remove the old package so the two cannot both be loaded:

npm uninstall tonder-web-sdk

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)

Before — a class you construct, then configure, then pay with

const checkout = new LiteInlineCheckout({
  mode: 'stage',
  apiKey: 'YOUR_KEY',
  returnUrl: 'https://merchant.example.com/return',
  callBack: (result) => handleResult(result),
});
await checkout.injectCheckout();

checkout.configureCheckout({
  customer: { firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com' },
  order_reference: 'ORD-001',
});

After — one factory, and the customer belongs to the session

const tonder = createTonder({
  api_key: tonderPublicConfig.api_key,
  environment: 'stage', // switch to 'production' when you go live
  session: {
    customer: { email: 'ada@example.com', first_name: 'Ada', last_name: 'Lovelace' },
  },
});
await tonder.init();
LegacyNewNote
mode: 'stage' | 'production'environment: 'stage' | 'production'Same two values, renamed key
apiKeyapi_keySame public key
injectCheckout()init()
configureCheckout({ customer })session.customer at creationNo longer a separate call
returnUrl on the constructorreturn_url per pay() callLets you build per-transaction return URLs
callBackevents.payment — or the promise pay() returnsSee below
order_referenceclient_referenceRenamed; your correlation carries over. See Correlation

Full reference: createTonder(config).

Results: callback or promise (client-side)

The legacy callBack fired for everything. The new SDK gives you both channels, and they are not alternatives:

  • pay() returns a promise with the transaction — use it to update the screen.
  • events.payment fires for every method, including flows with no promise. One set of handlers covers the whole checkout.

A decline is not an error in either: it arrives as a transaction with a declined status, on on_completed. on_error means no transaction exists at all. Event detail in the SDK README.

Path A: from LiteInlineCheckout

You already build your own UI, so this is a rename pass plus one behavioural change.

1. Map the methods (client-side)

LegacyNew
injectCheckout()init()
mountCardFields(request)tonder.create('card_fields', options).mount()
unmountCardFields(context)card_fields.unmount()
revealCardFields(request)card_fields.reveal(input)
getCustomerCards()getCustomerCards()
saveCustomerCard()enrollCard()
removeCustomerCard(skyflowId)removeCustomerCard(card_id)
getCustomerPaymentMethods()getPaymentMethods()
payment(data)pay(input)
verify3dsTransaction()— the SDK resolves 3DS itself

card_id and unmount_context keep the same meaning they had in mountCardFields, including the all / none / current values.

2. Mount card fields (client-side)

Before

await checkout.mountCardFields({ /* field config */ });

After — the container ids are the same defaults the legacy SDK used

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

Every configured field needs its container in the DOM before mount(), or the call rejects with MOUNT_COLLECT_ERROR.

Styles, labels and placeholders move to customization.card_fields on createTonder() — they are no longer passed to the mount call. See customization.card_fields.

3. Charge (client-side)

Before

checkout.configureCheckout({ customer, order_reference: 'ORD-001' });
const result = await checkout.payment({ /* cart */ });

After

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

Saved cards use { type: 'saved_card', card_id }; APMs use their method code. Full field list: tonder.pay(input).

4. Drop verify3dsTransaction() (client-side)

The legacy flow required you to call it on the return page. The new SDK resolves 3DS itself and gives you a choice of presentation:

  • presentation_mode: 'redirect' — the browser navigates, as it does today
  • presentation_mode: 'embedded' — a modal, and the shopper never leaves your page

See Presentation mode.

5. Optional: add Apple Pay (client-side)

Not available in the legacy SDK at all, so this is new capability rather than a migration.

One thing is worth starting now rather than at the end: ask Tonder to enable Apple Pay and register your domains. Tonder handles Apple — you never contact them and you do not need an Apple developer account. Your part is sending the list of domains and hosting one file Tonder gives you. It is not a code step, but it gates everything, and it is the most common reason a finished integration does not work in production.

Then follow the Apple Pay guide.

Path B: from InlineCheckout

InlineCheckout drew the entire checkout: the card form, the saved-card list, the APM picker, and the styling around them. The new SDK does not draw a checkout. You build the UI; the SDK gives you secure inputs for card data and the data to render everything else.

That is the whole of this migration. Every capability you had is still there — what changes is who renders it.

1. Inventory what the old UI gave your shoppers (planning)

Before writing code, list which of these your checkout actually showed. You rebuild only those.

The old UI drewYou now renderThe SDK gives you
Card formYour own layoutSecure inputs via create('card_fields') — you never touch raw PAN
Saved-card listYour own list and selectorgetCustomerCards() — masked number, brand, expiry, subscription_id
Save-card checkboxYour own checkboxenrollCard()
APM pickerYour own listgetPaymentMethods() — includes each method's label and logo URL
SafetyPay bank pickerYour own selectorgetPaymentMethodBanks() — grouped into cash and transfer
Pay button, copy, colorsYours
Loading and error statesYoursError codes to branch on

You do not have to design payment-method icons. getPaymentMethods() returns a logo URL per method, which is what the old UI rendered.

2. Replace the three lifecycle methods (client-side)

Before — the SDK owned the screen

const checkout = new InlineCheckout({ mode, apiKey, returnUrl, callBack });
await checkout.injectCheckout();   // draws everything into your container
checkout.setCallback(handleResult);
checkout.removeCheckout();

After — you own the screen; the SDK owns card security and the charge

You had one container and the SDK filled it with an entire checkout. Now you lay out your own form, and the SDK mounts a secure iframe into each card input. Everything around them — labels, the pay button, the saved-card list, the APM picker — is your markup.

<!-- your form, your layout; only these five are the SDK's -->
<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 id="pay">Pay</button>
const tonder = createTonder({ /* …see Setup… */ });
await tonder.init();

const card_fields = tonder.create('card_fields');
await card_fields.mount();          // into the containers above

const transaction = await tonder.pay({ /* …see Path A step 3… */ });

card_fields.unmount();              // your teardown, in place of removeCheckout()

Those five ids are the defaults; every configured field needs its container in the DOM before mount(), or the call rejects with MOUNT_COLLECT_ERROR. Give them a max-height in your CSS so the iframes do not grow before they settle.

3. Build the flows you inventoried (client-side)

They are the same flows the old UI walked your shopper through:

FlowGuide
New cardQuick start: card payment
Saved cardPay with a saved card
Save a cardSave a new card
APMs and SafetyPayAlternative payment methods
Apple Pay — new, not in the legacy SDKApple Pay

One rule worth carrying into your own UI: subscription_id on a saved card decides whether you need a CVV. Present means charge it directly; null means mount the saved-card CVV field first.

4. Styling (client-side)

The old customization had two halves. Only one has a counterpart:

Legacy customizationNow
Secure field styles, labels, placeholderscustomization.card_fields on createTonder()
Checkout UI: sections shown/hidden, colors, button copyYour own CSS — there is no SDK equivalent

Webhooks (server-side)

Both paths need this, and it is the part that is not a rename.

Today you receive two different payload shapes — one for card payments, another for APMs. You will now receive one, the same for every payment method: the Short format, the same one API Direct sends. Both formats are detailed in the Webhooks model and the events catalog.

Card webhooks

Legacy fieldNew fieldNote
transaction_referenceGone. Nothing in the new payload replaces it
payment_idGone
checkout_idGone. There is no checkout object on the new path
idNew, and the one that matters. Tonder's identifier for the transaction — what you pass to getTransaction() and quote to support
transaction_idNew. A Tonder-internal id for the processing record. Not what you correlate on
client_referenceNew. Your own order reference, in the webhook itself. This is what you correlate your order on
status and transaction_statusstatusThe legacy pair was duplicated; there is one now
amount, currency, metadatasame namesCarry over
providerproviderCarries over
transaction_typeoperation_type
operation_datecreatedISO 8601 now
number_of_payment_attemptsGone
response (nested, provider-specific)Gone. Read status and the decline fields instead
event_typee.g. payment_Success
payment_method_typee.g. CARD, SPEI, OXXO
actione.g. MODIFY

APM webhooks

Before — wrapped, and a different shape from card webhooks

{
  "action": "payment",
  "type": "apm",
  "data": { "transaction_status": "Success", "payment_id": 41714, "checkout_id": "…" }
}

After — identical in shape to a card webhook

{
  "id": "fc38522e-…",
  "operation_type": "payment",
  "status": "Success",
  "payment_method_type": "SPEI",
  "client_reference": "ORD-001",
  "transaction_id": "e9340a04-…",
  "event_type": "payment_Success"
}

If you branch on type === 'apm' or unwrap data, delete that code. One handler now covers every method.

What to do

  1. Read the current payload spec: How webhooks work and the Webhooks model.
  2. Correlate on client_reference instead of checkout_id or payment_id.
  3. Keep the handler idempotent by storing processed event ids — the retry policy and Dead Letter Queue are in Delivery & retries.
  4. Run both old and new endpoints during the cutover if you are migrating gradually; the payloads are distinguishable by the presence of event_type.

Correlation (server-side)

Your order reference keeps working. It is renamed, and it still identifies the same thing on Tonder's side — reconciliation you already have does not need rethinking, only the field name changes.

Field
LegacyconfigureCheckout({ order_reference })
Newpay({ client_reference })

metadata is supported by both, unchanged.

One thing to check before you migrate. Neither SDK merges these fields — the order reference and metadata are sent separately, and always have been. But in exported transactions reports, the Business Transaction ID column prefers metadata.order_id and falls back to the order reference only when it is absent. So if you have been sending both with different values, your webhooks correlate on one identifier and your reports on the other. Decide which one your reconciliation actually reads before mapping it, and send the same value in both from now on. The full metadata table is in tonder.pay(input).

Test the migration

Run in stage before switching production traffic.

CheckWhat proves it worked
New card paymentTransaction created through /process/, not the router
Declined cardArrives as a transaction with a declined status, not a thrown error
3DSResolves without you calling verify3dsTransaction()
Saved card with subscription_idCharges without a CVV prompt
Saved card without subscription_idPrompts for CVV, then charges
EnrollmentCard appears in getCustomerCards() afterwards
An APMReturns Pending, settles by webhook
Webhook handlerProcesses the new flat payload for both a card and an APM
Correlationclient_reference from your order appears in the webhook

The last two are the ones worth writing an explicit test for. Everything above them fails loudly; a webhook mismatch fails silently, and you find out when an order is not fulfilled.

Next steps

Was this page helpful?

On this page