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
code | Means | What to do |
|---|---|---|
APPLE_PAY_UNSUPPORTED_BROWSER | The browser cannot run Apple Pay | Offer another payment method — the only code that is the shopper's browser |
APPLE_PAY_NOT_ENABLED | Apple Pay is not enabled for your business | Yours to fix — see step 1 |
NOT_INITIALIZED | You called before init() finished | Yours 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:
| Key | Example |
|---|---|
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
| Check | What proves it worked |
|---|---|
| The sheet opens on a real device | The iOS Simulator cannot test web Apple Pay |
| A decline | Arrives on on_completed with a declined status, not on on_error |
client_reference | Appears 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.
