Managed Apple Pay
Accept Apple Pay on Tonder's Merchant ID: the checklist, domain registration, the Web SDK button, troubleshooting and testing.
Managed Apple Pay runs on Tonder's Apple Merchant ID and certificate: you send Tonder your
domains, host one verification file, and the Web SDK button (@tonder.io/web-sdk) does the
rest. 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 has two integration surfaces: the Web SDK's button component, described on this page,
and Hosted Checkout 2.0, where you only add apple_pay to
payment_method_types when you create the session and
Tonder's page shows the button. Apple Pay does not exist in the legacy SDK. On the Direct API,
Apple Pay is Bring Your Own Apple Pay: you obtain
the token under your own Merchant ID and send it to Tonder.
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.
Before you start: checklist
Almost every Apple Pay problem is setup, not code. Go through these items in order; if something still fails, Troubleshooting by symptom tells you which item it is.
On Hosted Checkout 2.0 the payment page runs on Tonder's domain, so only Your Tonder account and How you test apply. With the Web SDK button on your own site, every item applies.
Your Tonder account
- Apple Pay is enabled for your business in each environment you use, sandbox and production. Ask your Tonder contact.
- Nothing to set up with Apple: in this model you don't need an Apple developer account, and Tonder registers your domains. Already have your own Merchant ID? That is Bring Your Own Apple Pay.
Your domains (Web SDK)
- Tonder has every domain that shows the button: production, staging and previews. Each subdomain is its own registration, and the domain that counts is the one in the address bar, not an iframe or CDN host.
- The verification file Tonder sent you is live at
https://<your-domain>/.well-known/<file name>, served the way Apple requires. - If your site filters traffic by country, IP or bot rules, Apple's servers can still reach
/.well-known/â see the ranges to allow. - Tonder has confirmed both conditions: Apple Pay enabled and domain verified.
Your code (Web SDK)
- The checkout is an HTTPS page at the top level, not inside an iframe.
- You
await tonder.init()and render the button only whentonder.isApplePayAvailable().availableistrue. <div id="tonder-apple-pay-button"></div>exists and is empty beforemount()runs.- The button's
paymentcallback is synchronous: noawaitbetween the tap and the sheet. - You read results on
events.payment; a decline arrives onon_completed. - Your card option stays visible next to the button.
How you test
- Safari on a real iPhone, iPad or Mac. The iOS Simulator renders the button and then closes the sheet on tap.
- The device has a card in Wallet, and its region is one where Apple Pay is available (Mexico is). A Mac needs Touch ID, or a nearby iPhone or Apple Watch with Apple Pay set up.
- In sandbox, the device is signed in to an Apple Sandbox Tester account with one of Apple's test cards. Tonder's test cards can't be added to Wallet.
- The shopper's IP, location or VPN doesn't matter. Only their Wallet does.
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 â in this model 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.
How to serve the verification file
Apple fetches the file from your server and matches it byte for byte, so the URL has to return the file itself:
| Requirement | Detail |
|---|---|
| Path | https://<your-domain>/.well-known/<file name>, with the exact name Tonder sent |
| Content | The file as Tonder sent it: not renamed, edited or re-saved |
| Protocol | HTTPS with a valid certificate, TLS 1.2 or later |
| Redirects and proxies | None: the URL serves the file directly |
| Authentication | None: no login, no token |
Content-Type | text/plain, or none at all |
Before you tell Tonder the file is live, open the URL and read the response body, not just the
status code. A single-page app with a catch-all route answers 200 with index.html, so the URL
looks healthy while serving the wrong bytes. Some hosts also hide dot-directories: if yours
doesn't serve /.well-known/ by default, configure it.
If your site restricts traffic by country or IP
Apple checks the file from its own servers, outside Mexico. If your site only accepts traffic
from Mexico, or a firewall, WAF, CDN or bot rule blocks unknown clients, the file loads fine for
you and Apple still can't read it â so the domain never verifies. Allow Apple's
domain-verification ranges on /.well-known/:
17.23.4.96/27
17.23.19.0/27
17.23.24.32/27
17.32.139.128/26
17.132.108.64/26
17.140.126.0/26
17.157.32.0/27
17.157.40.128/27
17.157.44.128/27
17.179.144.128/25
17.253.0.0/16Apple publishes this list in Setting up your server and updates it from time to time; check it there before you configure the rule. Only your team can change this â Tonder isn't in the request path.
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.
Where the button goes in your checkout
Apple Pay earns its conversion lift by letting the shopper skip the card form entirely â so the layout that works is the express one: the button first, a divider, and your other methods underneath, untouched.
Apple Pay is one more payment method, not a replacement. Keep your credit and debit card
button â and every other method â visible alongside it. A shopper whose browser or wallet can't
use Apple Pay still needs a way to pay: that's exactly the
APPLE_PAY_UNSUPPORTED_BROWSER case.
The practices below follow Apple's Human Interface Guidelines:
| Practice | Why |
|---|---|
| Offer it early â express position, above the card form | Its value is skipping the form; burying it behind a "choose your method" step throws that away |
| Give it at least the prominence of your primary pay button | Same width or larger, and never below the fold when the card button isn't |
Match your checkout's controls with customization.apple_pay_button | width, height and border_radius are the sanctioned way to fit it in |
| Only render it when it's available | Never show a disabled or grayed-out Apple Pay button |
| Never rebuild or decorate it | The container stays empty and Safari's native control is the only permitted rendering |
The mistake to avoid is the replacement: swapping the card button out for Apple Pay strands every shopper it isn't available for. Measured checkouts keep both.
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.
Troubleshooting by symptom
| What you see | Why | Fix |
|---|---|---|
No button; availability returns APPLE_PAY_UNSUPPORTED_BROWSER | Not Safari, no card in Wallet, or the iOS Simulator | Test in Safari on a real device; your other methods cover the shopper meanwhile |
No button; availability returns APPLE_PAY_NOT_ENABLED | Apple Pay isn't enabled for your business | Ask Tonder â see step 1 |
No button; availability returns NOT_INITIALIZED | The check ran before init() finished | await tonder.init() first |
mount() throws APPLE_PAY_CONTAINER_NOT_FOUND | The container isn't on the page when mount() runs | Render the empty <div> before mounting |
The sheet opens and closes; on_error reports APPLE_PAY_VALIDATION_ERROR | Apple won't issue a session for the domain: file missing or altered, a redirect, or a firewall blocking Apple | Serve the file, allow Apple's ranges, and ask Tonder to confirm the domain |
The sheet never opens; on_error reports APPLE_PAY_SESSION_ERROR | The browser refused to open a payment sheet | Top-level HTTPS page, not an iframe, and nothing awaited before the sheet |
| Works in staging, not in production | The production domain isn't registered | Each domain is its own registration â send it to Tonder |
Enablement and domain verification are separate steps and fail differently: without enablement
no button ever renders (APPLE_PAY_NOT_ENABLED); without a verified domain the button renders and
the sheet closes (APPLE_PAY_VALIDATION_ERROR). Ask Tonder to confirm both.
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.
Apple's sandbox cards, the Sandbox Tester account and the device setup are on the Apple Pay overview. They are the same for both models.
Next steps
Bring Your Own Apple Pay
Your own Merchant ID and certificate, through the Direct API.
Apple Pay demo
The real button, live â open it in Safari.
Web SDK (guide)
Install, quick start, and alternative methods.
Migrate from Direct API
Apple Pay first, without touching your current checkout.
Web SDK Reference
Every method, type, and error.
Webhooks
Confirm the payment server-side.
