Skip to content

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 installationdomain_name needs to be configured before anything here works.
  • Domain configuration for Apple Pay — publishing the .well-known file, required in every integration model.
  • Appmax JS — the full init signature, 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

  1. Installation authorized with domain_name (or domain_names).
  2. .well-known file published on the domain.
  3. Script (scripts.appmax.com.br/appmax.min.js in 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.

SelectorRole
.appmax-apple-pay-btnContainer 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:

html
<div class="appmax-apple-pay-btn"></div>

The SDK injects something like:

html
<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, a v-if/conditional — the click won't do anything. No error, no log.
  • If the component that calls init can re-render (StrictMode, Fast Refresh, a poorly-scoped effect dependency), remember that init() 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.

html
<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.

javascript
const onUpdate = () => ({
  total: 129.90,
  freight: 15.00,
  discount: 10.00,
  installments: 1,
  products: [
    { name: 'Black T-shirt', price: 62.45, quantity: 2 },
  ],
});
FieldTypeRequiredWhat shows up on the PaymentSheet
totalnumberYesTotal line. The label is hardcoded to "Total" and is not configurable.
productsarrayRecommendedOne line per item. Without it, the sheet shows the total only.
products[].namestringYes, if products is presentLine label.
products[].pricenumberYes, if products is presentUnit price, in BRL.
products[].quantitynumberYes, if products is presentQuantity. The displayed value is price × quantity.
freightnumberNo"Frete" line. Omitted when 0 or absent.
discountnumberNo"Desconto" line. Omitted when 0 or absent.
installmentsnumberNoDoes 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: 12990 charges R$ 12,990.00, not R$ 129.90.
  • total: '129.90' (string) makes the SDK throw Error: 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:

javascript
// ✗ 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 PromiseWhat the SDK does
ResolvescompletePayment(STATUS_SUCCESS) — the sheet closes with Apple's confirmation.
RejectscompletePayment(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:

javascript
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

  1. 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.
  2. If the user changes shipping, installments, or an item along the way, Apple calls your onUpdate to refresh the displayed total.
  3. On confirmation (Face ID/Touch ID), the SDK calls your onAuthorize(appleToken).
  4. In onAuthorize, with the appleToken in hand: create the customer, create the order, and settle the payment at POST /v1/payments/apple-pay — the full appleToken → payload mapping is in the "Process the payment" section of Apple Pay payment.

Quick troubleshooting

SymptomLikely cause
Button doesn't appearOutside 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 opensThe 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 buyeronAuthorize caught the error without rethrowing. See "How to signal failure in onAuthorize".
Click does nothing, no errorThe 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 codeThe 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 onUpdate contract, DOM/init order, or a missing externalId.
  • Appeared and failed midway → the problem is merchant validation: the installation's domain_name, the .well-known file, or external_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.