Skip to content

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.

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

Put the element where the buttons should appear:

<kustom-express-buttons id="express" locale="sv-SE"></kustom-express-buttons>
AttributeRequiredWhat it does
localeYes, in practiceThe 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-modeNoprogressive (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.

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.

onError receives { code, message }:

codeMeans
NO_EXPRESS_METHODSNo 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_ABORTEDYour createOrder called reject. The purchase stops.
MERCHANT_HOOK_INVALID_RESPONSEYour createOrder resolved without an orderId — an integration bug rather than a decision.
PAYMENT_SUBMIT_FAILED, PAYMENT_FAILEDThe wallet payment didn’t go through.
DEPENDENCY_LOAD_FAILEDThe 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.

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.

no order yetan order existsPayPal is a step earlier: its popup can't open without an order
Your server, at pay timeYour server, before the page loadsKustom, from the browser
You passcreateOrderorderIdorderLines only
FitsA product page “buy now”, a cart or mini cartBeside a checkout you have already created an order forA prototype with no backend
The order’s merchant_urlsYours, from your create callYours, from your create callThe URLs in the Portal’s Express buttons setup
Prices and tax come fromYour serverYour serverThe browser — anyone can change them
An order existsOnly once the customer starts payingFor every page view, whether anyone pays or notOnly once the customer starts paying
When the cart changesThe order is built fresh at pay time; only the displayed total can go staleReplace the element and initialize it againInitialize 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.

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.

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

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.

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.

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.

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.

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

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.

This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.