> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Express Checkout: shipping in the Apple Pay and Google Pay sheets

> Supply shipping options and confirm the shopper's choice from inside the Apple Pay and Google Pay sheets.

<Note>
  **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](/docs/checkout/primer-checkout/installation)). iOS and Android are coming.

  Because it is new, **test your integration end to end in the [sandbox](/docs/testing/overview) before going live in production.**
</Note>

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.

<Info>
  **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.
</Info>

## Prerequisites

* Primer Checkout already integrated. See [First payment](/docs/checkout/primer-checkout/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](/docs/connections/payment-methods/apple-pay/overview).
* An [API key](/docs/api-reference/get-started/authentication) 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.

```typescript theme={"dark"}
const checkout = document.querySelector('primer-checkout');

checkout.options = {
  applePay: {
    // Apple Pay nests these under `shippingOptions`.
    shippingOptions: {
      requireShippingMethod: true,
      requiredShippingContactFields: ['postalAddress', 'name', 'email'],
    },
  },
  googlePay: {
    // Google Pay takes them at the top level. The nesting is not the same
    // as Apple Pay, so do not copy the block above.
    captureShippingAddress: true,
    requireShippingMethod: true,
  },
};
```

<Warning>
  **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.
</Warning>

| Option                                                   | Wallet     | Effect                                                  |
| -------------------------------------------------------- | ---------- | ------------------------------------------------------- |
| `applePay.shippingOptions.requireShippingMethod`         | Apple Pay  | Shows the shipping method selector in the sheet.        |
| `applePay.shippingOptions.requiredShippingContactFields` | Apple Pay  | Which shipping contact fields the shopper must provide. |
| `googlePay.captureShippingAddress`                       | Google Pay | Collects a shipping address in the sheet.               |
| `googlePay.requireShippingMethod`                        | Google Pay | Shows the shipping option 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.

<Warning>
  **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.
</Warning>

```typescript theme={"dark"}
checkout.addEventListener('primer:shipping-address-change', (event) => {
  // Tell the SDK to wait. Required whenever you respond asynchronously.
  event.preventDefault();

  const { shippingAddress, setShippingOptions } = event.detail;

  (async () => {
    const rates = await fetch('/my-backend/shipping-rates', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ shippingAddress }),
    }).then((r) => r.json());

    setShippingOptions(rates);
  })();
});
```

### Event payload

`primer:shipping-address-change` is a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). Its data is on `event.detail`.

| Property             | Type                                  | Description                                                                                                                                                                                                                |
| -------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodType`  | `'APPLE_PAY' \| 'GOOGLE_PAY'`         | Which wallet the shopper is using.                                                                                                                                                                                         |
| `shippingAddress`    | `NullableAddress`                     | The address from the sheet, redacted by the wallet at this stage. Expect city, state, postal code and country. Street address, name, phone and email are not available until after authorization. Any field can be `null`. |
| `setShippingOptions` | `(options: ShippingOption[]) => void` | Call with the options to display. Call it once. Later calls are ignored.                                                                                                                                                   |
| `timestamp`          | `number`                              | Unix timestamp in seconds, when the event was dispatched.                                                                                                                                                                  |

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

```typescript theme={"dark"}
interface ShippingOption {
  id: string;           // Stable identifier. Echoed back on selection, and
                        // must match order.shipping.methodId when you confirm.
  name: string;         // Display label, e.g. "Standard"
  description: string;  // Display detail, e.g. "5-7 business days"
  amount: number;       // Shipping cost in MINOR UNITS, e.g. 500 = £5.00
}
```

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()`.

```typescript theme={"dark"}
checkout.addEventListener('primer:shipping-option-change', (event) => {
  event.preventDefault();

  // `continue` is a reserved word in JavaScript. Alias it when destructuring.
  const { selectedShippingOption, continue: confirmShipping } = event.detail;

  (async () => {
    const response = await fetch('/my-backend/commit-shipping', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      // Send the id only. Your backend resolves the price.
      body: JSON.stringify({ shippingOptionId: selectedShippingOption.id }),
    });

    if (!response.ok) {
      // Do not call continue() here. The PATCH did not land, so the session
      // still holds the old shipping. Acknowledging anyway would let the
      // shopper reach authorization and fail there, which is harder to
      // understand. Log it and let the callback time out instead.
      console.error('Shipping PATCH failed, not confirming the selection');
      return;
    }

    confirmShipping();
  })();
});
```

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

| Property                 | Type                          | Description                                                                                  |
| ------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| `paymentMethodType`      | `'APPLE_PAY' \| 'GOOGLE_PAY'` | Which wallet the shopper is using.                                                           |
| `selectedShippingOption` | `ShippingOption`              | The option that was selected, exactly as you supplied it.                                    |
| `continue`               | `() => void`                  | Acknowledge the confirmation. **Reserved word** — give it an alias, as in the example above. |
| `timestamp`              | `number`                      | Unix timestamp in seconds, when the event was dispatched.                                    |

### The PATCH

Your backend PATCHes `order.shipping` onto the client session:

```bash theme={"dark"}
curl -X PATCH https://api.sandbox.primer.io/client-session \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4" \
  -H "Content-Type: application/json" \
  -d '{
    "clientToken": "YOUR_CLIENT_TOKEN",
    "orderId": "order-123",
    "order": {
      "shipping": {
        "methodId": "standard",
        "methodName": "Standard",
        "methodDescription": "5-7 business days",
        "amount": 500
      },
      "lineItems": [
        { "itemId": "sku-1", "description": "T-shirt", "amount": 2000, "quantity": 1 }
      ]
    }
  }'
```

<Warning>
  **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.
</Warning>

### Tax

Primer recomputes the order total as:

```
sum over line items of (quantity x amount + taxAmount - discountAmount)
+ shipping.amount
+ any fees
```

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.

| Callback                         | If you do not respond in time                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `primer:shipping-address-change` | The sheet shows a recoverable error and stays open. The shopper can try another address.                                   |
| `primer:shipping-option-change`  | The sheet shows a recoverable error, and the payment attempt then fails at authorization. The confirmation is not retried. |

### When something goes wrong

Failures in this flow surface on `primer:payment-failure`, like any other payment failure:

```typescript theme={"dark"}
checkout.addEventListener('primer:payment-failure', (event) => {
  const { error } = event.detail;
  console.error(error.origin, error.code, error.message, error.suggestion);
});
```

* `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](/docs/checkout/primer-checkout/guides-and-recipes/log-errors-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

<CardGroup cols={2}>
  <Card title="Events reference" icon="bolt" href="/docs/sdk/primer-checkout-web/events-reference">
    Full reference for the shipping events and every other checkout event
  </Card>

  <Card title="Apple Pay" icon="apple" href="/docs/connections/payment-methods/apple-pay/overview">
    Configure Apple Pay, including merchant identity and domain verification
  </Card>

  <Card title="Google Pay" icon="google" href="/docs/connections/payment-methods/google-pay/overview">
    Configure Google Pay
  </Card>

  <Card title="Update client session" icon="pen-to-square" href="/docs/api-reference/v2.4/api-reference/client-session-api/update-client-session">
    API reference for the PATCH used to commit shipping
  </Card>
</CardGroup>
