Accept wallet payments with Express Checkout
Use the Express Checkout Element to offer Apple Pay and Google Pay from a merchant-owned payment page. Paypercut checks wallet availability, renders the eligible buttons, creates an opaque Payment Method, and keeps the native wallet pending while your backend starts the payment.
This guide requires @paypercut/checkout-js 1.4.0 or later. Use the Card Element as a fallback for customers whose browser or device does not provide an eligible wallet.
How Express Checkout works
- Your frontend creates an Elements Group in
paymentmode and mounts one Express Checkout Element. - Paypercut checks Apple Pay and Google Pay availability and displays eligible buttons.
- The customer opens a wallet and authorizes the payment.
- The Element emits
confirmwith an opaque Payment Method and any requested customer details. - Your frontend sends the Payment Method ID and merchant-owned order ID to your backend.
- Your backend validates the order, creates and confirms a custom Checkout Session, and returns its
client_secretwhen client-side continuation may be required. - Your frontend calls
paypercut.confirmPayment({ clientSecret })when needed, then completes or fails the native wallet.
The Express Checkout Element owns wallet capability checks, provider buttons, secure collection, Payment Method creation, and supported customer authentication. Your integration owns placement, order validation, shipping calculations, server-side payment confirmation, and fulfillment.
Before you begin
You need:
- a Paypercut account with publishable and secret API keys;
- an HTTPS payment page in production;
- a backend endpoint that validates and persists orders;
- a verified webhook endpoint;
- Apple Pay domain registration if you enable Apple Pay.
See Apple Pay setup for domain requirements.
1. Install the SDK
npm install @paypercut/checkout-js@^1.4.0
import { Paypercut } from '@paypercut/checkout-js';
For a classic browser script, pin the exact version:
<script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.4.0/dist/paypercut-checkout.iife.min.js"></script>
2. Create and mount the Element
Add a container for wallet buttons and a status region:
<div id="express-checkout"></div>
<p id="wallet-status" role="status"></p>
Create the Element from the same Paypercut client and Elements Group used by the rest of your custom checkout:
const paypercut = Paypercut({
publishableKey: 'YOUR_PUBLISHABLE_KEY',
});
const elements = paypercut.elements({
mode: 'payment',
amount: 2999,
currency: 'EUR',
locale: 'auto',
});
const expressCheckout = elements.create('expressCheckout', {
paymentMethods: {
applePay: 'auto',
googlePay: 'auto',
},
layout: {
maxColumns: 2,
maxRows: 1,
overflow: 'auto',
},
buttonHeight: 48,
emailRequired: true,
billingAddressRequired: true,
});
expressCheckout.on('ready', ({ availablePaymentMethods }) => {
const unavailable =
!availablePaymentMethods.applePay && !availablePaymentMethods.googlePay;
document.querySelector('#express-checkout').hidden = unavailable;
});
expressCheckout.on('error', ({ code }) => {
document.querySelector('#wallet-status').textContent =
`Wallet payment unavailable: ${code}`;
});
expressCheckout.mount('#express-checkout');
Express Checkout is available only when the Elements Group uses mode: 'payment'. Keep the Card Element or another payment method available when no wallet is eligible.
3. Supply current order details
The optional click event runs immediately before the wallet opens. If you register it, call exactly one of resolve() or reject() while the provider's user gesture is active.
expressCheckout.on('click', async (event) => {
try {
const order = await fetch('/api/orders/current').then((response) => response.json());
event.resolve({
lineItems: order.lineItems,
emailRequired: true,
billingAddressRequired: true,
});
} catch {
event.reject();
}
});
Without a click listener, the Element continues using its current options and the amount configured on the Elements Group.
Update commerce data for the next wallet sheet without remounting:
expressCheckout.update({
lineItems: [
{ name: 'Order total', amount: 3498 },
],
emailRequired: true,
});
elements.update({ amount: 3498 });
Keep the group amount, displayed order total, and backend Checkout Session amount consistent.
4. Start the payment from confirm
The confirm event supplies the wallet-backed Payment Method. Send its opaque ID and your order ID to your backend:
expressCheckout.on('confirm', async (event) => {
try {
const response = await fetch('/api/orders/order_1042/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method: event.paymentMethod.id,
billing_details: event.billingDetails,
shipping_address: event.shippingAddress,
shipping_rate: event.shippingRate?.id,
}),
});
const serverResult = await response.json();
if (!response.ok) throw new Error(serverResult.message || 'Payment failed.');
if (serverResult.clientSecret) {
const result = await paypercut.confirmPayment({
clientSecret: serverResult.clientSecret,
});
if (
!['succeeded', 'processing', 'requires_capture'].includes(
result.checkoutSession.paymentObjectStatus ?? '',
)
) {
throw new Error(
`Payment status: ${result.checkoutSession.paymentObjectStatus}`,
);
}
}
event.complete();
window.location.assign(serverResult.returnUrl);
} catch (error) {
event.paymentFailed({
code: 'merchant_checkout_failed',
message: error instanceof Error ? error.message : 'Payment failed.',
});
}
});
Call exactly one of event.complete() or event.paymentFailed() after your backend responds. The native wallet remains pending until you do.
Do not call elements.submit() or paypercut.createPaymentMethod() for the Payment Method supplied by the Express Checkout confirm event. The Element already performed secure collection and Payment Method creation.
The optional event data includes:
| Field | Description |
|---|---|
wallet |
apple_pay or google_pay. |
paymentMethod |
Opaque { type: 'payment_method', id } reference for your backend. |
paymentMethodPreview |
Browser-safe card display details such as brand and last four digits. |
billingDetails |
Requested name, email, phone, and billing address when supplied by the wallet. |
shippingAddress |
Final shipping details when shipping collection is enabled. |
shippingRate |
Final wallet-selected shipping rate. |
Do not log or place the Payment Method ID or Checkout Session client secret in a URL, UI markup, analytics, or client-side storage.
5. Create and confirm the Session on your backend
Your backend must reload the order from its own database, validate the wallet-supplied shipping choice, and calculate the final amount. Then:
- Create a Checkout Session with
ui_mode=custom. - Store its ID before confirmation.
- Confirm it with
event.paymentMethod.idand a separate idempotency key. - Return the Checkout Session
client_secretonly to the untrusted client instance and customer that own the payment attempt. - Reconcile the authenticated confirmation response and verified webhooks before fulfillment.
See Build a custom checkout with Elements for the complete backend requests, idempotency guidance, and client-side continuation flow.
Collect shipping details
Enable shipping only for orders that require physical fulfillment:
const expressCheckout = elements.create('expressCheckout', {
shippingAddressRequired: true,
allowedShippingCountries: ['DE', 'AT', 'NL'],
shippingRates: [
{
id: 'standard',
displayName: 'Standard delivery',
amount: 499,
detail: '3–5 business days',
},
],
});
Wallets may provide only a redacted address while their sheet is open. Use shippingaddresschange to calculate eligible rates and a new total, then call resolve() or reject() promptly:
expressCheckout.on('shippingaddresschange', async (event) => {
try {
const quote = await fetch('/api/shipping/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: event.address }),
}).then((response) => response.json());
event.resolve({
amount: quote.amount,
lineItems: quote.lineItems,
shippingRates: quote.shippingRates,
});
} catch {
event.reject();
}
});
When the customer changes the selected rate, recalculate the total through shippingratechange:
expressCheckout.on('shippingratechange', async (event) => {
const quote = await calculateShippingRate(event.shippingRate.id);
event.resolve({
amount: quote.amount,
lineItems: quote.lineItems,
shippingRates: quote.shippingRates,
});
});
Always validate the final address and shipping rate again on your backend before confirming payment.
Options reference
| Option | Values or type | Purpose |
|---|---|---|
paymentMethods.applePay |
auto, always, or never |
Controls whether Apple Pay is considered during availability checks. |
paymentMethods.googlePay |
auto, always, or never |
Controls whether Google Pay is considered during availability checks. |
layout |
maxColumns, maxRows, overflow |
Controls the wallet button grid. |
buttonType |
Provider-approved button labels | Sets Apple Pay and Google Pay call-to-action variants. |
buttonTheme |
Provider-approved themes | Sets Apple Pay and Google Pay color variants. |
buttonHeight |
CSS pixels | Sets button height. |
buttonRadius |
CSS pixels | Sets the shared button corner radius. |
emailRequired |
boolean | Requests the customer's email. |
phoneNumberRequired |
boolean | Requests the customer's phone number. |
billingAddressRequired |
boolean | Requests a billing address. |
shippingAddressRequired |
boolean | Requests a shipping address. |
allowedShippingCountries |
ISO country code array | Restricts eligible shipping countries. |
lineItems |
{ name, amount }[] |
Supplies order rows in minor currency units. |
shippingRates |
Shipping rate array | Supplies initial wallet shipping choices. |
Provider selection, layout, and button styling are immutable for an Element's lifetime. Use expressCheckout.update() for mutable commerce data used by the next wallet gesture.
Events reference
Every on() call returns an unsubscribe function.
| Event | Use |
|---|---|
ready |
Read initial wallet availability and show or hide the container. |
availablepaymentmethodschange |
React when Apple Pay or Google Pay availability changes. |
resize |
Observe the hosted component height. |
click |
Validate the order and provide current wallet-sheet details before opening. |
shippingaddresschange |
Quote shipping for a redacted in-sheet address. |
shippingratechange |
Recalculate totals for the selected shipping rate. |
confirm |
Send the created Payment Method to your backend and settle the wallet UI. |
cancel |
Restore any merchant UI changed for the wallet attempt. |
error |
Show another payment method or retry guidance using code and recoverable. |
Test and clean up
Use sandbox credentials and test wallet accounts during development. Wallet eligibility depends on the browser, device, merchant domain, and wallet configuration; always provide a fallback.
Destroy the Elements Group when the owning page or framework component permanently unmounts:
window.addEventListener('pagehide', () => elements.destroy(), { once: true });
Destroying the browser component does not cancel or reverse a server-side payment. Reconcile the order before starting another attempt.

