Skip to main content
New feature. Express Checkout shipping is available to all merchants. No allowlisting or account setup is required.What it covers today: Apple Pay and Google Pay, on the Web SDK (Primer Checkout). iOS and Android are coming.Because it is new, test your integration end to end in the sandbox before going live in production.
Apple Pay and Google Pay collect the shopper’s shipping address inside the native payment sheet, after checkout has already started. If your shipping rates depend on that address, you need to price them while the sheet is open. Express Checkout shipping does this with two events. Your page supplies the shipping options for the address the shopper entered, and your backend confirms the option they pick. Primer then recalculates the order total from the confirmed shipping, so the amount stays authoritative on our side.

When to use it

Use it when shipping cost or availability depends on the destination and cannot be known when the client session is created. For example:
  • Physical goods priced by weight, zone, or carrier rate.
  • Free-shipping thresholds that vary by region.
  • Shipping choices that depend on state held in the browser, such as a selected store or a delivery window.

How it works

  1. The shopper enters an address. The checkout emits primer:shipping-address-change. The sheet stays in a loading state.
  2. Your page returns options by calling setShippingOptions(). This is display only. Primer does not store the list and does not price anything from it.
  3. An option is selected. The checkout emits primer:shipping-option-change with that option. This fires when the shopper picks one, and also for the option the sheet pre-selects, so expect it more than once per sheet session.
  4. Your backend confirms it by PATCHing order.shipping onto the client session, then your page calls continue().
  5. Primer re-reads the session, checks that the confirmation matches the selection, recomputes order.amount, and refreshes the sheet total.
  6. The shopper authorizes. Authorization waits for the confirmation, so a payment can never be created against a shipping option your backend has not priced.
The two events are a pair. primer:shipping-address-change is display only and never changes the amount. primer:shipping-option-change is the one that moves money, and it goes through your backend.

Prerequisites

  • Primer Checkout already integrated. See First payment.
  • @primer-io/primer-js 1.9.2 or later. Earlier versions do not dispatch these events at all.
  • Apple Pay and/or Google Pay configured in the Dashboard.
  • An API key with client_tokens:write for the client session PATCH.

Step 1. Enable shipping in the payment sheet

Shipping selection is off by default. Turn it on for each wallet in the SDK options.
The two wallets nest these options differently. Apple Pay reads applePay.shippingOptions.requireShippingMethod. Google Pay reads googlePay.requireShippingMethod, with no shippingOptions object. An option placed at the wrong level is ignored silently, and the sheet opens without a shipping selector.

Step 2. Supply options for the address

Listen for primer:shipping-address-change and call setShippingOptions() with the options you want the sheet to show.
Call preventDefault() before any async work. The SDK treats the event as handled only if your listener calls preventDefault(), or calls setShippingOptions() synchronously. If you fetch rates from your backend, setShippingOptions() runs later, so preventDefault() is the only thing telling the SDK to wait. Without it the SDK moves on with no options and no shipping confirmation, no error is raised, and the shopper is charged the total without shipping. The same rule applies to continue() in Step 3.

Event payload

primer:shipping-address-change is a CustomEvent. Its data is on event.detail. The wallet gives you a partial address on purpose. If you price below postal code granularity, you cannot do it at this stage.

The ShippingOption shape

Return an empty array to tell the shopper you do not ship to that address. The sheet shows an “address not serviceable” error and they can enter a different one.

Step 3. Confirm the selection from your backend

When an option is selected, the checkout emits primer:shipping-option-change. Send the selection to your backend, which PATCHes it onto the client session, then call continue().
Your confirm endpoint can be called more than once during a single sheet session, including for the option the sheet pre-selects before the shopper touches anything. Make it idempotent.

Event payload

The PATCH

Your backend PATCHes order.shipping onto the client session:
Do not send a top-level amount. It pins the order total and overrides Primer’s recalculation. The shopper is charged the total without shipping, and the shipping you just confirmed is dropped silently.

Tax

Primer recomputes the order total as:
If the destination changes the tax due, set taxAmount on each line item and re-send the full lineItems array in the same PATCH. Two things to watch:
  • taxAmount is added once per line. It is not multiplied by quantity.
  • A top-level order.taxAmount is not included in the recalculated total. Use per line item tax, or a fee.

Step 4. Primer verifies and refreshes

After continue(), the SDK re-reads the client session and checks that order.shipping.methodId and order.shipping.amount match the option that was selected. If they match, the sheet total updates and the shopper can authorize. If they do not, the attempt fails with an integration error.

Lifecycle and edge cases

Timeouts

Each callback has 20 seconds to respond. Apple and Google also apply their own limit inside the sheet, which we do not control, so treat 20 seconds as a ceiling rather than a budget. Keep rate calls and confirmations fast.

When something goes wrong

Failures in this flow surface on primer:payment-failure, like any other payment failure:
  • error.origin is integration when the fix is on your side, and primer when it is on ours.
  • error.code is REQUEST_TIMEOUT when a callback was not answered in time, and REQUEST_ERROR when the confirmation did not match the selection.
  • error.suggestion carries a short, actionable hint. Read it first.
See Log errors and debugging for the wider error model.

No listener responded

If nothing responds, either because no listener is registered or because an async listener did not call preventDefault(), the SDK logs a console warning and carries on by itself. No options are shown, no shipping is confirmed, and the shopper is charged the total without shipping. If the amounts look wrong, check the browser console for a warning about primer:shipping-address-change or primer:shipping-option-change having no listener.

Retries

If a payment fails and the shopper reopens the wallet sheet, the shipping flow starts again from the address change. Options supplied in a previous attempt are not reused.

Changing the address twice

If the shopper edits the address again before the previous confirmation has completed, that earlier confirmation is dropped and the flow restarts from the new address. Your backend can therefore receive a PATCH for an address the shopper has already moved on from. Always price from the address in the request you are handling, never from stored state.

See also

Events reference

Full reference for the shipping events and every other checkout event

Apple Pay

Configure Apple Pay, including merchant identity and domain verification

Google Pay

Configure Google Pay

Update client session

API reference for the PATCH used to commit shipping