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 when tonder.isApplePayAvailable().available is true.
  • <div id="tonder-apple-pay-button"></div> exists and is empty before mount() runs.
  • The button's payment callback is synchronous: no await between the tap and the sheet.
  • You read results on events.payment; a decline arrives on on_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:

RequirementDetail
Pathhttps://<your-domain>/.well-known/<file name>, with the exact name Tonder sent
ContentThe file as Tonder sent it: not renamed, edited or re-saved
ProtocolHTTPS with a valid certificate, TLS 1.2 or later
Redirects and proxiesNone: the URL serves the file directly
AuthenticationNone: no login, no token
Content-Typetext/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/16

Apple 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

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.

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.

Checkout
Business licence$250.00 MXN
Credit or debit card

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:

PracticeWhy
Offer it early — express position, above the card formIts 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 buttonSame width or larger, and never below the fold when the card button isn't
Match your checkout's controls with customization.apple_pay_buttonwidth, height and border_radius are the sanctioned way to fit it in
Only render it when it's availableNever show a disabled or grayed-out Apple Pay button
Never rebuild or decorate itThe 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 seeWhyFix
No button; availability returns APPLE_PAY_UNSUPPORTED_BROWSERNot Safari, no card in Wallet, or the iOS SimulatorTest in Safari on a real device; your other methods cover the shopper meanwhile
No button; availability returns APPLE_PAY_NOT_ENABLEDApple Pay isn't enabled for your businessAsk Tonder — see step 1
No button; availability returns NOT_INITIALIZEDThe check ran before init() finishedawait tonder.init() first
mount() throws APPLE_PAY_CONTAINER_NOT_FOUNDThe container isn't on the page when mount() runsRender the empty <div> before mounting
The sheet opens and closes; on_error reports APPLE_PAY_VALIDATION_ERRORApple won't issue a session for the domain: file missing or altered, a redirect, or a firewall blocking AppleServe the file, allow Apple's ranges, and ask Tonder to confirm the domain
The sheet never opens; on_error reports APPLE_PAY_SESSION_ERRORThe browser refused to open a payment sheetTop-level HTTPS page, not an iframe, and nothing awaited before the sheet
Works in staging, not in productionThe production domain isn't registeredEach 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

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.

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

Was this page helpful?

On this page