Implementing the Apple Pay button with the Appmax JS
This guide covers the part of the Apple Pay flow that runs in the browser, via appmax.min.js: how the button is rendered, the DOM contract the script requires, and how to wire the callbacks up to payment. For the rest of the flow, see:
- App installation —
domain_nameneeds to be configured before anything here works. - Domain configuration for Apple Pay — publishing the
.well-knownfile, required in every integration model. - Appmax JS — the full
initsignature, IP collection, and card tokenization (this guide assumes you've already read the "How to use" section there). - Apple Pay payment — the endpoint contract (payload, Apple Token mapping, integrated model × direct flow, FAQ).
Quick prerequisites
- Installation authorized with
domain_name(ordomain_names). .well-knownfile published on the domain.- Script (
scripts.appmax.com.br/appmax.min.jsin production) loaded on the page.
Without all three, the button may still show up, but domain validation fails when the PaymentSheet tries to open.
Rendering the button
appmax.min.js recognizes two DOM selectors. You only need one of them — not both.
| Selector | Role |
|---|---|
.appmax-apple-pay-btn | Container you render empty. The SDK replaces its innerHTML with Apple's official button (SVG and styles included). Recommended path — you don't need to draw the button yourself. |
[data-appmax-apple-pay] | The button itself. This is the element the SDK registers the click on. Use this attribute directly on your own button if you'd rather control the markup. |
Simplest path — let the SDK draw the button:
<div class="appmax-apple-pay-btn"></div>The SDK injects something like:
<button data-appmax-apple-pay class="applepay-button">
<span class="applepay-button__label">Pay with</span>
<svg class="applepay-button__logo">...</svg>
</button>The button only appears if the device supports Apple Pay
Outside Safari (or on a device with no Apple Pay set up), ApplePaySession.canMakePayments() returns false and the SDK doesn't activate the button — it may stay empty or hidden, depending on how you styled the container. That's expected, not a bug.
Load order: the button needs to exist before init
AppmaxScripts.init(...) looks for the button once, at the moment it runs, and doesn't observe DOM changes after that — the same contract described in "DOM contract" on the Appmax JS page. For the Apple Pay button, the practical consequence is:
- If the container/button only mounts after
init— behind a route, a checkout step, av-if/conditional — the click won't do anything. No error, no log. - If the component that calls
initcan re-render (StrictMode, Fast Refresh, a poorly-scoped effect dependency), remember thatinit()is not idempotent: each call registers a new listener on top of the previous one.
Practical rule for SPAs: make sure the button's container is already in the DOM at the exact moment AppmaxScripts.init(...) is called — never the other way around.
Initializing with the Apple Pay callbacks
For Apple Pay, init requires externalId, onUpdate, and onAuthorize (plus onSuccess/onError, always required). The full reference for each parameter is in Appmax JS → Initialize AppmaxScripts; the focus here is how these three connect to the Apple Pay flow specifically.
<div class="appmax-apple-pay-btn"></div>
<script>
// onUpdate: called when the PaymentSheet opens and whenever the user
// changes something in it. Must return the current cart, with numeric
// values in BRL — see "What onUpdate must return", below.
const onUpdate = () => ({
total: 129.90,
freight: 15.00,
discount: 10.00,
installments: 1,
products: [{ name: 'Black T-shirt', price: 62.45, quantity: 2 }],
});
// onAuthorize: called when the payment is authorized by the user.
// Receives the Apple Token — the tokenized card, ready for
// POST /v1/payments/apple-pay (see the link above for the full payload).
// Reject the Promise to signal failure — see "How to signal failure",
// below.
const onAuthorize = async (appleToken) => {
await processPayment(appleToken);
};
const onSuccess = (data) => { /* ip collected, if applicable */ };
const onError = (err) => console.error('Apple Pay error:', err);
window.AppmaxScripts.init(onSuccess, onError, externalId, onUpdate, onAuthorize);
</script>Parameter order and names
init(onSuccess, onError, externalId, onUpdate, onAuthorize) — in that order. Swapping onUpdate and onAuthorize, or writing onAutorize, makes the SDK treat the wrong parameter as the callback function.
externalId is the same external_id you returned with an HTTP 200 during app installation — without it, init throws a synchronous exception (see the warning in Appmax JS).
What onUpdate must return
onUpdate is called when the PaymentSheet opens and whenever the user changes something in it. The returned object describes the cart, with numeric values in BRL — the SDK converts it into the format Safari's PaymentSheet consumes. You don't build the lineItems or format the total.
const onUpdate = () => ({
total: 129.90,
freight: 15.00,
discount: 10.00,
installments: 1,
products: [
{ name: 'Black T-shirt', price: 62.45, quantity: 2 },
],
});| Field | Type | Required | What shows up on the PaymentSheet |
|---|---|---|---|
total | number | Yes | Total line. The label is hardcoded to "Total" and is not configurable. |
products | array | Recommended | One line per item. Without it, the sheet shows the total only. |
products[].name | string | Yes, if products is present | Line label. |
products[].price | number | Yes, if products is present | Unit price, in BRL. |
products[].quantity | number | Yes, if products is present | Quantity. The displayed value is price × quantity. |
freight | number | No | "Frete" line. Omitted when 0 or absent. |
discount | number | No | "Desconto" line. Omitted when 0 or absent. |
installments | number | No | Does not affect the PaymentSheet. Accepted for compatibility. |
Values are numbers in BRL — not cents, not strings
This is the most common cause of the PaymentSheet failing to open.
total: 12990charges R$ 12,990.00, not R$ 129.90.total: '129.90'(string) makes the SDK throwError: Error processing payment...before the sheet opens, because it calls.toFixed(2)on the value.
For the same reason, do not return the already-formatted object:
// ✗ Wrong — this is the INTERNAL format, which the SDK builds from your return value
{ total: { label: 'Total', amount: { currency: 'BRL', value: '129.90' } },
displayItems: [ /* ... */ ] }
// ✓ Correct — the cart, with numbers
{ total: 129.90, products: [{ name: 'Black T-shirt', price: 62.45, quantity: 2 }] }How to signal failure in onAuthorize
The SDK decides the PaymentSheet outcome from the state of the Promise your onAuthorize returns:
| Your Promise | What the SDK does |
|---|---|
| Resolves | completePayment(STATUS_SUCCESS) — the sheet closes with Apple's confirmation. |
| Rejects | completePayment(STATUS_FAILURE) — the sheet reports the failure to the buyer. |
return false does not signal failure
Only rejecting the Promise is read as a failure. A try/catch around your backend call — the natural pattern for showing an error message on screen — makes the Promise resolve, and the buyer sees Apple's confirmation on a payment that was declined.
If you need to handle the error, rethrow it:
const onAuthorize = async (appleToken) => {
try {
const res = await fetch('/checkout/apple-pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId, customer_id: customerId, appleToken }),
});
if (!res.ok) throw new Error('Payment declined');
} catch (err) {
showError(err);
throw err; // required: without this, the sheet closes as a success
}
};From click to payment
- The user clicks the button → the SDK opens the
ApplePaySession(Safari's PaymentSheet) and validates the merchant automatically — you don't call any endpoint for this. - If the user changes shipping, installments, or an item along the way, Apple calls your
onUpdateto refresh the displayed total. - On confirmation (Face ID/Touch ID), the SDK calls your
onAuthorize(appleToken). - In
onAuthorize, with theappleTokenin hand: create the customer, create the order, and settle the payment atPOST /v1/payments/apple-pay— the fullappleToken→ payload mapping is in the "Process the payment" section of Apple Pay payment.
Quick troubleshooting
| Symptom | Likely cause |
|---|---|
| Button doesn't appear | Outside Safari, or the device has no card set up in Apple Wallet. Confirm with ApplePaySession.canMakePayments(). |
Error processing payment... in onError and the PaymentSheet never opens | The onUpdate return value is off-contract — usually values in cents, as strings, or the already-formatted object. See "What onUpdate must return". |
| A declined payment shows up as approved to the buyer | onAuthorize caught the error without rethrowing. See "How to signal failure in onAuthorize". |
| Click does nothing, no error | The button was mounted after init (SPA) — see "Load order" above. |
init() throws Error: External ID is required... | externalId missing or invalid — it's a synchronous exception, it does not go through onError. |
Generic Safari error on confirmation (e.g. DOMException) | Invalid merchant session — check whether the domain has the .well-known file published (guide) and whether external_id is the installation's latest one. |
| Works on one store but not another, same code | The second store's domain wasn't informed during installation (domain_name) or doesn't have the .well-known file published. |
Did the sheet never open, or open and then fail?
That's the first question to ask, and it separates two worlds that never overlap:
- Never appeared → the problem is in the browser, before any network call: the
onUpdatecontract, DOM/initorder, or a missingexternalId. - Appeared and failed midway → the problem is merchant validation: the installation's
domain_name, the.well-knownfile, orexternal_id.
You can't test the full flow on localhost — see Testing and Sandbox for environment details.
Reference project for implementation
The appmaxbrasil/appstore-demo-php repository contains a reference implementation of the full Apple Pay integration — App Store installation, button rendering and appmax.js callbacks on the checkout, and the call to the payment endpoint — in plain PHP and vanilla JavaScript, with no framework. Use it as a study base and a starting point for your own implementation, not as production code.
The repository's Apple Pay walkthrough links directly to the code line corresponding to each step described in this guide.