- Checkout
- Features
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
Section titled “Before you start”- Elements installed on a registered domain — see Adding Elements to your site.
- 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 configured on your Merchant ID. The wallet sheet’s shipping options come from it.
- For PayPal, a connected PayPal account. Without one the PayPal button never appears; the others are unaffected.
What every option shares
Section titled “What every option shares”Place the tag
Section titled “Place the tag”Put the element where the buttons should appear:
<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
Section titled “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:
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
Section titled “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.
Handle errors
Section titled “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
Section titled “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.
createOrderYour server, at pay timeorderIdYour server, before the page loadsYour server creates the order before the page renders, on every page view whether the customer pays or not. Push and confirmation go to your URLs.orderLinesKustom, from the browserKustom creates the order from the lines in the page when the customer picks an address, or on the tap for PayPal. Push and confirmation go to the Portal's URLs.| 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
Section titled “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.
- The page loadsYour page → Kustomexpress.initialize with display lines and createOrderKustom → Your pagethe wallet buttons
- The customer picks an addressCustomer → Your pagetaps a wallet and picks an addressPayPal: on the tap, before any addressYour page → Your serveryour createOrder hook calls your endpointYour server → KustomPOST/
checkout/ v3/ orders Kustom → Your serverorder_idYour server → Your pageresolve({ orderId })Kustom → Your pageshipping options and the real total - The customer paysCustomer → Your pageconfirms in the walletYour page → Kustomcomplete the orderKustom → Your serverpush, to your push URLYour page → CustomeronConfirm redirects to your confirmation page
In the browser, add orderLines for the sheet to show and a createOrder hook:
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 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 anyasyncfunction, is never heard from and the purchase waits forever. There is no timeout.rejectaborts 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
createOrderwhen the customer first chooses an address — so withoutshippingAddressRequired: truethey never call it at all. PayPal runs on its own SDK, and its popup can’t open until it has an order, so it callscreateOrderon the tap: before any address, and whether or not the customer goes on to pay. orderLinesare 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 asquantity, is ignored, so put the quantity intototalAmount.- 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_countriesto 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 ownshipping_options— is only right for the country you created the order for. - Leave
options.date_of_birth_mandatoryoff. A wallet sheet can’t collect a date of birth, so an order that requires one can’t complete, and the purchase fails withPAYMENT_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
Section titled “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.
- Before the page rendersYour server → KustomPOST/
checkout/ v3/ orders Kustom → Your serverorder_idYour server → Your pagethe page, carrying the order_id - The page loadsYour page → Kustomexpress.initialize with orderIdKustom → Your pagethe order's lines, currency and the wallet buttons
- The customer paysCustomer → Your pagepicks an address and confirms in the walletYour page → Kustomcomplete the orderKustom → Your serverpush, to the order's push URLYour page → CustomeronConfirm redirects to your confirmation page
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
Section titled “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.
- The page loadsYour page → Kustomexpress.initialize with orderLinesKustom → Your pagethe wallet buttons
- The customer picks an addressCustomer → Your pagetaps a wallet and picks an addressPayPal: on the tap, before any addressYour page → Kustomthe order lines from the pageKustom creates the order from themKustom → Your pageshipping options and the real total
- The customer paysCustomer → Your pageconfirms in the walletYour page → Kustomcomplete the orderKustom → Your serverpush, to the Portal's push URLYour page → CustomeronConfirm redirects to the Portal's confirmation URL
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
Section titled “Check it works”On a registered domain, load the page in a browser that supports at least one of your wallets, with Playground credentials:
- At least one button renders. If the element stays empty, see Troubleshooting.
- 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. - Pay.
onConfirmfires, the browser lands on the confirmation page, and the push arrives.
Troubleshooting
Section titled “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
Section titled “Related”- Confirmation and push — what to do with the order once the customer lands on your page
- Adding Elements to your site — the script and domain allowlist this depends on
- Element catalogue — the other elements and the locales they accept
- Create the order — the server call your
createOrderendpoint makes - Shipping Assistant — where the sheet’s shipping options come from
This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.