Payment Methods

Apple Pay

Accept Apple Pay on the web with the Web SDK button: domain enablement, availability, events, and reconciliation.

Apple Pay is accepted on the web through the Web SDK button (@tonder.io/web-sdk). The SDK renders the button, presents Apple's payment sheet, and processes the charge; the transaction lands on /process/ with the same shape as a card payment and fires the same webhooks — your reconciliation does not change.

Apple Pay does not exist in the legacy SDK or as a Direct API payment_method.type. The only integration surface is the Web SDK's button component.

Try it live: the Apple Pay demo runs this page's code — open it in Safari (macOS, iOS or iPadOS) to see the real button and the availability result.

Step 1: ask Tonder to enable it and register your domains

Not a code step, and the most common reason a correct integration fails in production.

  • Tonder handles Apple — you never contact them and you do not need an Apple developer account.
  • Your part is sending Tonder the list of domains that will show the button — subdomains count separately — and hosting the verification file Tonder gives you.

Start this step before writing code: it gates everything else. The exact steps and failure modes are in the SDK README.

Step 2: render the button

Apple Pay is the one flow that does not end in a pay() call. 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-only mechanism — they fire for every method the SDK charges, so one set of handlers covers the whole checkout.

The SDK renders the button into an element you provide. It has to exist before mount() runs, and it stays empty — do not put a button, a label, or an icon inside it:

<div id="tonder-apple-pay-button"></div>
const tonder = createTonder({
  api_key: tonderPublicConfig.api_key,
  environment: 'stage', // switch to 'production' when you go live
  session: { customer: { email: 'ada@example.com' } },
  // Fires for EVERY payment method, not just Apple Pay. With pay() these run
  // alongside the promise it returns; Apple Pay has no promise, so for it
  // these callbacks are the only channel.
  events: {
    payment: {
      on_completed: (transaction) => handleResult(transaction),
      on_error: (error) => showError(error.code),
      on_cancel: () => { /* shopper dismissed the sheet — not an error */ },
    },
  },
});

await tonder.init();

// Ask before you render. The answer is an OBJECT, never a bare boolean:
// { available: true }, or { available: false, code, message }. Reading the
// object itself as a condition would always be truthy — read .available.
const availability = tonder.isApplePayAvailable();

if (availability.available) {
  const button = tonder.create('apple_pay_button', {
    // Called SYNCHRONOUSLY when the shopper taps, so it reads whatever the
    // cart holds at that moment — amount, currency and references can all
    // change after mount without remounting the button. It must not be
    // async: Apple requires the sheet to open in the same tick as the tap.
    payment: () => ({
      amount: 250,
      currency: 'MXN',
      return_url: 'https://merchant.example.com/return',
      client_reference: 'ORD-001',
    }),
  });
  await button.mount();
} else {
  // Do not guess at the reason: only APPLE_PAY_UNSUPPORTED_BROWSER means
  // "offer another method"; the other two codes are yours to fix.
  console.info('Apple Pay hidden:', availability.code, availability.message);
}

When availability is false

codeMeansWhat to do
APPLE_PAY_UNSUPPORTED_BROWSERThe browser cannot run Apple PayOffer another payment method — the only code that is the shopper's browser
APPLE_PAY_NOT_ENABLEDApple Pay is not enabled for your businessYours to fix — see step 1
NOT_INITIALIZEDYou called before init() finishedYours to fix — await tonder.init()

Customize the button

Safari draws the control natively, so Apple only allows these keys in customization.apple_pay_button — anything else is ignored:

KeyExample
type'check-out' (the call to action)
style'black'
locale'es-MX' (the label's language)
width / height'100%' / '48px'
border_radius'8px'

The SDK reads customization once at createTonder() — changing it means building a new instance and mounting again. The same applies to api_key and session.customer.

A settled attempt consumes its references, whatever the outcome. On on_completed, mint a fresh client_reference and idempotency_key so the next tap is a new order with its own idempotency scope.

What this guide leaves out — a custom container id and releasing the button on a route change — is in Apple Pay in the SDK README.

pay({ payment_method: { type: 'apple_pay' } }) is rejected on purpose. Apple Pay cannot be charged through pay() — Apple's gesture requirement is why. Use the button component.

Step 3: your reconciliation does not change

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. See the Webhooks model.

Test Apple Pay

CheckWhat proves it worked
The sheet opens on a real deviceThe iOS Simulator cannot test web Apple Pay
A declineArrives on on_completed with a declined status, not on on_error
client_referenceAppears in the webhook and correlates your order

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

Next steps

Was this page helpful?

On this page