# Add express buttons

Add Apple Pay, Google Pay and PayPal express buttons to a product or cart page, and choose who creates the order — createOrder, orderId or orderLines.

Express buttons let a customer pay from a product page or a cart with a wallet they already
use, without opening the full checkout. The wallet sheet collects the address and the
payment, Kustom prices shipping and completes the order, and you send the customer to your
confirmation page. Unlike the other Elements, the tag alone does nothing: a JavaScript call
tells the buttons which order they're paying for, and there are three ways to supply it.

## Before you start

- **Elements installed** on a registered domain — see
  [Adding Elements to your site](/checkout/surfaces/elements/).
- **The Express buttons setup in the Portal.** Under **Elements**, open **Express buttons**
  and fill in the push URL and confirmation URL (the validation URL is optional). They're
  required whichever way you create the order. Without them the element renders nothing.
- **[Shipping Assistant](/checkout/shipping/#shipping-assistant)** configured on your Merchant
  ID. The wallet sheet's shipping options come from it.
- **For PayPal**, a [connected PayPal account](/checkout/paypal/). Without one the PayPal
  button never appears; the others are unaffected.

## What every option shares

### Place the tag

Put the element where the buttons should appear:

```html
<kustom-express-buttons id="express" locale="sv-SE"></kustom-express-buttons>
```

| Attribute | Required | What it does |
|---|---|---|
| `locale` | Yes, in practice | The market, as `language-COUNTRY`. The country picks the purchase country and filters the wallets to those enabled for that market. Left out, it defaults to a bare `en` with no market at all. |
| `reveal-mode` | No | `progressive` (the default) shows each button as it becomes ready; `full` holds a loading placeholder until all of them are. |

The `id` isn't special — it's only what the selector in `initialize` points at. Any CSS
selector works, and only the first element it matches is initialized.

Which buttons appear is decided for you: the methods chosen in the Portal, narrowed to the
ones enabled for the market, then to the ones the customer's browser can actually offer.
Apple Pay, for instance, only appears where Apple Pay is available.

### Initialize it

One call configures the element. These fields are the same in every option. What you add to
them — `createOrder`, `orderId` or `orderLines` — is the choice [below](#who-creates-the-order):

```js
window.kustomElements('express.initialize', '#express', {
  currency: 'SEK',
  shippingAddressRequired: true,
  // + createOrder, orderId or orderLines
  onConfirm: (result) => {
    window.location.href = result.redirectUrl;
  },
  onError: (error) => {
    console.error(error.code, error.message);
  },
});
```

**Leave `shippingAddressRequired` set to `true`.** It's what brings Shipping Assistant's
options into the wallet sheet, and the `createOrder` hook depends on it.

You can call `initialize` again on the same element — when the cart changes, for instance. A
changed configuration rebuilds the buttons; an identical one only swaps in the new callbacks.

### Send the customer to your confirmation page

When the purchase completes, `onConfirm` receives `{ orderId, status, redirectUrl }`, where
`redirectUrl` is the order's confirmation URL with its placeholders filled in. **The element
never navigates on its own** — it holds a "finalizing your purchase" overlay over the page
until you do — so redirect in `onConfirm`.

From there, the order is an ordinary Checkout order: the push arrives, and you acknowledge
and fulfill it the way you do for the embedded checkout — see
[Confirmation and push](/checkout/integrate/confirmation-and-push/).

### Handle errors

`onError` receives `{ code, message }`:

| `code` | Means |
|---|---|
| `NO_EXPRESS_METHODS` | No wallet could be shown for this market, configuration and browser. Hide whatever surrounds the buttons. Also fired as an `expressError` DOM event on the element. |
| `MERCHANT_ABORTED` | Your `createOrder` called `reject`. The purchase stops. |
| `MERCHANT_HOOK_INVALID_RESPONSE` | Your `createOrder` resolved without an `orderId` — an integration bug rather than a decision. |
| `PAYMENT_SUBMIT_FAILED`, `PAYMENT_FAILED` | The wallet payment didn't go through. |
| `DEPENDENCY_LOAD_FAILED` | The wallet provider's script couldn't load in the browser. |

`onError` only hears about problems once the buttons exist. If `initialize` itself fails — an
unregistered domain, no Express buttons setup in the Portal, an unknown `orderId`, a
selector that matches nothing — the element stays empty, `onError` isn't called, and the
promise `kustomElements(...)` returned never settles. Don't hold the rest of the page on it.

The browser is also the only place these errors appear. If you want them in your own logs,
forward them from `onError`.

## Who creates the order

Every express purchase completes a normal Kustom Checkout order. What you choose is who
creates it and when — and that decides whose `merchant_urls` it carries, where its prices
come from, and how many orders you make that nobody pays for.

| | **Your server, at pay time** | **Your server, before the page loads** | **Kustom, from the browser** |
|---|---|---|---|
| You pass | `createOrder` | `orderId` | `orderLines` only |
| Fits | A product page "buy now", a cart or mini cart | Beside a checkout you have already created an order for | A prototype with no backend |
| The order's `merchant_urls` | Yours, from your create call | Yours, from your create call | The URLs in the Portal's Express buttons setup |
| Prices and tax come from | Your server | Your server | The browser — anyone can change them |
| An order exists | Only once the customer starts paying | For every page view, whether anyone pays or not | Only once the customer starts paying |
| When the cart changes | The order is built fresh at pay time; only the displayed total can go stale | Replace the element and initialize it again | Initialize again with the new lines |

**Use `createOrder` unless you have a reason not to.** It's the only option where your server
builds the order from your own cart at the moment the customer pays, and the order goes
through the same push, acknowledge and capture handling as every other order you create.
Kustom's own platform add-ons use it for that reason. Letting Kustom create the order instead
is the usual way purchases end up at the wrong confirmation page, because the order takes the
Portal's URLs rather than the ones your shop would have sent.

If you pass more than one, `orderId` wins over `createOrder`, and `createOrder` over plain
`orderLines`.

### Your server creates it at pay time

The element asks your code for an order at the last moment, and your server creates it from
the cart it already holds. Nothing exists until the customer is paying, and nothing the page
says about prices reaches the order.

```mermaid
sequenceDiagram
    actor Customer
    participant Page as Your page
    participant Server as Your server
    participant Kustom

    critical The page loads
        Page->>Kustom: express.initialize with display lines and createOrder
        Kustom-->>Page: the wallet buttons
    end

    critical The customer picks an address
        Customer->>Page: taps a wallet and picks an address
        Note over Customer,Page: PayPal: on the tap, before any address
        Page->>Server: your createOrder hook calls your endpoint
        Server->>Kustom: POST /checkout/v3/orders
        Kustom-->>Server: order_id
        Server-->>Page: resolve({ orderId })
        Kustom-->>Page: shipping options and the real total
    end

    critical The customer pays
        Customer->>Page: confirms in the wallet
        Page->>Kustom: complete the order
        Kustom-)Server: push, to your push URL
        Page->>Customer: onConfirm redirects to your confirmation page
    end
```

In the browser, add `orderLines` for the sheet to show and a `createOrder` hook:

```js
window.kustomElements('express.initialize', '#express', {
  currency: 'SEK',
  shippingAddressRequired: true,
  // Shown in the wallet sheet until the real order exists. Not sent to the order.
  orderLines: [
    { name: 'Wool sweater', type: 'physical', totalAmount: 79900, taxAmount: 15980,
      productUrl: 'https://my-shop.com/p/wool-sweater' },
  ],
  createOrder: (resolve, reject) => {
    fetch('/express/order', { method: 'POST' })
      .then((res) => (res.ok ? res.json() : Promise.reject(res.status)))
      .then((order) => resolve({ orderId: order.order_id }))
      .catch(reject);
  },
  onConfirm: (result) => {
    window.location.href = result.redirectUrl;
  },
  onError: (error) => {
    console.error(error.code, error.message);
  },
});
```

On your server, `/express/order` builds the order from the customer's cart and
[creates it](/checkout/integrate/create-order/) with `POST /checkout/v3/orders`, exactly as for
the embedded checkout, then returns its `order_id`. The browser sends no prices.

Four things about this hook that aren't obvious from its shape:

- **Call `resolve({ orderId })` — don't return it.** The hook is run as a promise executor,
  so a function that *returns* the id, including any `async` function, is never heard from
  and the purchase waits forever. There is no timeout. `reject` aborts the purchase.
- **It runs on the address, not the tap — except for PayPal.** Apple Pay, Google Pay, Klarna
  and Amazon Pay share one wallet sheet, which opens without an order and calls `createOrder`
  when the customer first chooses an address — so without `shippingAddressRequired: true` they
  never call it at all. PayPal runs on its own SDK, and its popup can't open until it has an
  order, so it calls `createOrder` on the tap: before any address, and whether or not the
  customer goes on to pay.
- **`orderLines` are for display only.** The sheet shows their total until the order exists,
  then switches to the order's real total with shipping, so make them as close to the final
  amount as you can. Amounts are in minor units and include tax. The fields are exactly the
  five above; anything else on a line, such as `quantity`, is ignored, so put the quantity
  into `totalAmount`.
- **Create a fresh order on every call.** If the customer closes one wallet and opens
  another, the hook runs again. Reusing an order from an earlier attempt fails once that
  order has moved on.

The order must be created under the same Merchant ID as the Elements script on the page, and
two of its settings matter more here than in the embedded checkout, because the wallet sheet
does less:

- **Limit `shipping_countries` to the purchase country.** Your server isn't told when the
  customer picks an address in the sheet, so anything you priced by country — tax, your own
  `shipping_options` — is only right for the country you created the order for.
- **Leave `options.date_of_birth_mandatory` off.** A wallet sheet can't collect a date of
  birth, so an order that requires one can't complete, and the purchase fails with
  `PAYMENT_FAILED`.

**On a product page**, the "buy now" a button offers is for that product, not the customer's
cart. Have your endpoint build the order from the product and quantity currently selected, and
leave the customer's cart alone, so a purchase from the product page doesn't empty a bag they
were filling. When the selection changes, initialize again with new `orderLines` so the sheet
shows the right total.

### Your server creates it before the page loads

If an order exists before the page renders — typically the one behind an embedded checkout on
the same page — hand the buttons its id. They read the lines and currency from the order, so
they always show what the order holds.

```mermaid
sequenceDiagram
    actor Customer
    participant Page as Your page
    participant Server as Your server
    participant Kustom

    critical Before the page renders
        Server->>Kustom: POST /checkout/v3/orders
        Kustom-->>Server: order_id
        Server->>Page: the page, carrying the order_id
    end

    critical The page loads
        Page->>Kustom: express.initialize with orderId
        Kustom-->>Page: the order's lines, currency and the wallet buttons
    end

    critical The customer pays
        Customer->>Page: picks an address and confirms in the wallet
        Page->>Kustom: complete the order
        Kustom-)Server: push, to the order's push URL
        Page->>Customer: onConfirm redirects to your confirmation page
    end
```

```js
window.kustomElements('express.initialize', '#express', {
  orderId: checkoutOrderId,
  currency: 'SEK',
  shippingAddressRequired: true,
  onConfirm: (result) => {
    window.location.href = result.redirectUrl;
  },
});
```

The order must belong to the same Merchant ID as the Elements script, and must not already be
completed. Any `createOrder` you pass alongside is ignored.

The cost is keeping the two in step. The element reads the order once, when it initializes.
When the order changes, the element doesn't notice — and calling `initialize` again with the
same id is ignored as an identical configuration. Replace the element with a fresh one and
initialize that.

### Kustom creates it from the browser

Pass the lines and nothing else, and Kustom creates the order from them when the customer
starts paying. Your server isn't involved until the push arrives.

```mermaid
sequenceDiagram
    actor Customer
    participant Page as Your page
    participant Server as Your server
    participant Kustom

    critical The page loads
        Page->>Kustom: express.initialize with orderLines
        Kustom-->>Page: the wallet buttons
    end

    critical The customer picks an address
        Customer->>Page: taps a wallet and picks an address
        Note over Customer,Page: PayPal: on the tap, before any address
        Page->>Kustom: the order lines from the page
        Note over Page,Kustom: Kustom creates the order from them
        Kustom-->>Page: shipping options and the real total
    end

    critical The customer pays
        Customer->>Page: confirms in the wallet
        Page->>Kustom: complete the order
        Kustom-)Server: push, to the Portal's push URL
        Page->>Customer: onConfirm redirects to the Portal's confirmation URL
    end
```

```js
window.kustomElements('express.initialize', '#express', {
  currency: 'SEK',
  shippingAddressRequired: true,
  orderLines: [
    { name: 'Wool sweater', type: 'physical', totalAmount: 79900, taxAmount: 15980,
      productUrl: 'https://my-shop.com/p/wool-sweater' },
  ],
  onConfirm: (result) => {
    window.location.href = result.redirectUrl;
  },
});
```

It needs no server code, which is why it's useful for trying the buttons out — and the reason
it shouldn't take real orders:

- **The order takes the Portal's URLs.** Push and confirmation go to the URLs in the Express
  buttons setup, not to anything you choose per order, and the order belongs to the Merchant ID
  the Elements script belongs to.
- **The amounts come from the browser.** Order lines in a page can be edited by anyone with
  developer tools, so check the amount against your own cart on push before you fulfill.
- **You hear about the order only from the push.** Nothing on your server created it.

When the cart changes, initialize again with the new lines. `merchantReference1` and
`shippingOptions` (fallback shipping options for when Shipping Assistant can't supply any)
are only used in this option.

## Check it works

On a registered domain, load the page in a browser that supports at least one of your
wallets, with Playground credentials:

1. At least one button renders. If the element stays empty, see
   [Troubleshooting](#troubleshooting).
2. Open the wallet sheet and choose an address. With `createOrder`, your server receives the
   request at that point (on the tap, for PayPal). The sheet's total changes to include
   shipping.
3. Pay. `onConfirm` fires, the browser lands on the confirmation page, and the push arrives.

## Troubleshooting

**The element stays empty.** In this order: the domain isn't registered in the Portal, the
Express buttons setup has no URLs, the locale has no country or names a market none of your
methods cover, or the selector passed to `initialize` matches nothing. None of these reach
`onError` — check the browser console for an unhandled rejection.

**The sheet opens but your server never sees `createOrder`.** `shippingAddressRequired`
isn't `true`, or the customer hasn't chosen an address yet. Only PayPal calls the hook on the
tap.

**The purchase sits on "finalizing" forever.** Your `createOrder` returned a value instead
of calling `resolve`, or the request it made never finished.

**One wallet is missing.** Apple Pay needs a browser and device that offer it; PayPal needs a
connected PayPal account; and every method has to be both chosen in the Portal and enabled
for the market the locale names.

## Related

- [Confirmation and push](/checkout/integrate/confirmation-and-push/) — what to do with the
  order once the customer lands on your page
- [Adding Elements to your site](/checkout/surfaces/elements/) — the script and domain
  allowlist this depends on
- [Element catalogue](/checkout/surfaces/elements-catalogue/) — the other elements and the
  locales they accept
- [Create the order](/checkout/integrate/create-order/) — the server call your `createOrder`
  endpoint makes
- [Shipping Assistant](/checkout/shipping/#shipping-assistant) — where the sheet's shipping
  options come from
