Express Checkout
Express Checkout lets you render Apple Pay and Google Pay wallet buttons directly on your page — no card form, no redirect. Customers tap a button, authenticate with Face ID, Touch ID, or their device PIN, and payment details are pulled from their wallet automatically.
The result is the fastest possible checkout experience for mobile and desktop users who already have a wallet configured on their device.
Live Demo
See it in action: Visit Express Checkout Demo to try a live Express Checkout flow before integrating.
Why Express Checkout?
Wallet payments consistently outperform card forms — especially on mobile, where typing card details is the single biggest source of checkout abandonment.
- Higher conversion — customers complete payment in 2 taps with Face ID or Touch ID, with no card number to type and no redirect to leave your page.
- Lower fraud — biometric authentication and device-bound credentials mean lower chargeback rates compared to manually entered card data.
- Zero PCI scope for card numbers — card data never touches your servers. Apple and Google handle encryption end-to-end; Paypercut handles the rest.
- Works everywhere wallets are — Apple Pay on Safari (iPhone, iPad, Mac), Google Pay on Chrome and Android. Buttons only appear when the customer's device and browser support them.
- One integration, two wallets — a single SDK call renders both Apple Pay and Google Pay. You write the code once.
Express Checkout vs. Standard Checkout
| Express Checkout | Standard Checkout (card form) | |
|---|---|---|
| Steps to pay | 2 taps (button + biometric) | Fill card number, expiry, CVC, billing address |
| Card form | None | Required |
| Redirect | None — stays on your page | Optional hosted page or iframe |
| Authentication | Face ID / Touch ID / device PIN | 3D Secure challenge (SMS OTP or app) |
| Card data on your servers | Never — Apple/Google encrypt end-to-end | Never — tokenised by Paypercut |
| PCI scope | Minimal | Minimal |
| Works on mobile | Native wallet UX | Typing on small keyboard |
| Works on desktop | Yes (Safari + Chrome) | Yes |
| Supports shipping & line items | Yes — shown inside wallet sheet | Handled by your own UI |
| Integration complexity | ~20 lines of JS | Card form + submit flow |
Express Checkout is the right choice when conversion on mobile matters. Standard Checkout gives you more control over the UI and supports customers who have not set up a wallet on their device. Most integrations offer both.
How It Works
- Your page loads the Paypercut JS SDK and mounts the Express Checkout buttons into a container element.
- Apple Pay and Google Pay buttons appear instantly — no checkout session needed upfront.
- The customer taps a wallet button. The native payment sheet opens (managed entirely by Apple / Google).
- The customer reviews the order, selects their card, billing address, and shipping option, then authenticates with biometrics or device PIN.
- The SDK fires a
payment_method_createdevent containing apaymentMethodId. - Your backend uses that
paymentMethodIdto create a Payment Intent via the Paypercut API and charge the customer. - Paypercut processes the payment and sends the result via webhook.
Important: The
paymentMethodIdis single-use. Your backend must create the Payment Intent promptly after receiving the event. See the Create Payment Intent API for the full request reference.
Prerequisites
Before integrating Express Checkout you need:
-
A Paypercut merchant account — Sign up and obtain your publishable API key from the dashboard under API Keys.
-
A Paypercut Customer — Express Checkout creates payment methods that are attached to a customer. You must create a customer server-side before or at the point of collecting payment:
POST /v1/customers Authorization: Bearer sk_live_... { "email": "user@example.com", "name": "Jane Doe" }Store the returned
customer.id— you will pass it when creating the Payment Intent. -
Apple Pay domain registration (for Apple Pay) — Register your domain in the Paypercut dashboard under Settings → Apple Pay. See the Apple Pay setup guide.## Installation
Install the Paypercut JS SDK via npm:
npm install @paypercut/checkout-js
Or load it from a CDN:
<script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.1.9/dist/paypercut-checkout.iife.min.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
Basic Integration
1. Add a container element
<div id="express-checkout"></div>
2. Initialize and mount
import { PaypercutExpressCheckout } from '@paypercut/checkout-js';
const express = PaypercutExpressCheckout({
publishableKey: 'pk_live_...',
amount: 2999, // €29.99 in minor units
currency: 'EUR',
countryCode: 'DE',
});
express.on('payment_method_created', async ({ paymentMethodId, wallet, billingDetails }) => {
// Send paymentMethodId to your backend and create a Payment Intent
const response = await fetch('/api/confirm-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentMethodId }),
});
const result = await response.json();
if (result.status === 'succeeded') {
// Show success UI
}
});
express.on('error', (err) => {
console.error('Express Checkout error:', err);
});
express.mount('#express-checkout');
3. Create the Payment Intent on your backend
POST /v1/payment_intents
Authorization: Bearer sk_live_...
{
"amount": 2999,
"currency": "EUR",
"payment_method": "pm_...", // paymentMethodId from the SDK event
"customer": "cus_...", // your Paypercut customer ID
"confirm": true
}
Options Reference
Pass these when calling PaypercutExpressCheckout(options):
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
publishableKey |
string |
Yes | — | Your Paypercut publishable API key |
amount |
number |
Yes | — | Order total in minor units (e.g. 1200 = €12.00) |
currency |
string |
Yes | — | ISO 4217 currency code (e.g. 'EUR', 'GBP') |
mode |
string |
No | 'payment' |
Payment mode. Currently only 'payment' is supported |
lineItems |
LineItem[] |
No | — | Order breakdown rows shown in the Apple Pay sheet (subtotal, tax, shipping). Google Pay shows the total only |
shippingOptions |
ShippingOption[] |
No | — | Initial shipping options shown in the wallet sheet |
requestShipping |
boolean |
No | false |
Request shipping address from the customer |
requestEmail |
boolean |
No | false |
Request email address from the customer |
requestName |
boolean |
No | false |
Request cardholder name from the customer |
countryCode |
string |
No | — | ISO 3166-1 alpha-2 merchant country (e.g. 'DE'). Used in wallet payment requests |
allowedCountries |
string[] |
No | — | Google Pay only — restrict the address picker to specific countries (e.g. ['DE', 'AT', 'CH']). Apple Pay does not support pre-filtering; reject unsupported addresses at runtime via status: 'invalid_shipping_address' in the shipping_address_change handler |
walletOptions |
string[] |
No | Both | Which wallets to enable: ['apple_pay'], ['google_pay'], or both |
layout |
string |
No | 'row' |
Button layout — see Button Layout |
locale |
string |
No | 'en' |
ISO 639-1 language code for button labels (e.g. 'de', 'fr', 'bg'). Unsupported values fall back to 'en' |
buttonOptions |
ButtonOptions |
No | — | Customize button appearance — see Button Appearance |
Line Items
Line items appear in the Apple Pay payment sheet as a breakdown below the total. Google Pay ignores this field.
lineItems: [
{ label: 'Subtotal', amount: 2499 },
{ label: 'Shipping', amount: 500, type: 'pending' }, // 'pending' shows a dash until confirmed
]
| Field | Type | Description |
|---|---|---|
label |
string |
Row label shown in the sheet |
amount |
number |
Amount in minor units |
type |
'final' | 'pending' |
'pending' shows a placeholder until the final amount is known |
Shipping Options
shippingOptions: [
{ id: 'standard', label: 'Standard Shipping', detail: '3–5 business days', amount: 499 },
{ id: 'express', label: 'Express Shipping', detail: 'Next day', amount: 999 },
]
| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier for the option |
label |
string |
Display name |
detail |
string |
Optional subtitle (delivery estimate, etc.) |
amount |
number |
Shipping cost in minor units |
Button Appearance
Control the look of the wallet buttons via the buttonOptions object:
const express = PaypercutExpressCheckout({
publishableKey: 'pk_live_...',
amount: 2999,
currency: 'EUR',
buttonOptions: {
type: 'buy', // button label
applePayStyle: 'black', // Apple Pay color
googlePayColor: 'black', // Google Pay color
borderRadius: '0.5rem', // rounded corners
},
});
Button Type (type)
The type controls the label text shown on both Apple Pay and Google Pay buttons.
| Value | Label shown |
|---|---|
'plain' (default) |
Just the wallet logo — no text |
'pay' |
"Pay with Apple Pay / Google Pay" |
'buy' |
"Buy with Apple Pay / Google Pay" |
'checkout' |
"Check out with Apple Pay / Google Pay" |
'donate' |
"Donate with Apple Pay / Google Pay" |
'subscribe' |
"Subscribe with Apple Pay / Google Pay" |
'book' |
"Book with Apple Pay" (Apple Pay only) |
'order' |
"Order with Apple Pay" (Apple Pay only) |
'tip' |
"Tip with Apple Pay" (Apple Pay only) |
'contribute' |
"Contribute with Apple Pay" (Apple Pay only) |
Apple Pay Button Style (applePayStyle)
| Value | Appearance |
|---|---|
'black' (default) |
Black button with white logo |
'white' |
White button with black logo |
'white-outline' |
White button with black border and logo |
Google Pay Button Color (googlePayColor)
| Value | Appearance |
|---|---|
'default' (default) |
Follows the user's system theme |
'black' |
Black button with white logo |
'white' |
White button with colored logo |
Border Radius (borderRadius)
Applies to both buttons. Accepts any valid CSS value:
borderRadius: '0' // sharp corners
borderRadius: '8px' // slightly rounded
borderRadius: '0.5rem' // default
borderRadius: '24px' // pill shape
Button Layout
Control how multiple wallet buttons are arranged:
const express = PaypercutExpressCheckout({
// ...
layout: 'row', // 'row' | 'column' | 'auto'
});
| Value | Behavior |
|---|---|
'row' (default) |
Buttons side by side, equal width |
'column' |
Buttons stacked vertically, full width |
'auto' |
SDK switches to 'column' at ≤540px container width, 'row' above |
You can also change the layout after mount:
express.setLayout('column');
Locale
Set the language for wallet button labels:
const express = PaypercutExpressCheckout({
// ...
locale: 'de', // German
});
Supported locales: bg, cs, el, en, hr, hu, pl, ro, sk, sl.
Apple Pay supports all values in this list. Google Pay falls back to 'en' for unsupported locales.
Change the locale dynamically after mount:
express.setLocale('pl');
Events
Subscribe to events using .on(). Each call returns an unsubscribe function.
const unsubscribe = express.on('payment_method_created', handler);
// later:
unsubscribe();
ready
Fired when wallet buttons have been rendered and are visible. Use this to hide a loading indicator.
express.on('ready', () => {
document.getElementById('spinner').remove();
});
session_start
Fired when the customer taps a wallet button, before the payment sheet opens. You must call updateWith() to unblock the sheet. Use this to apply dynamic pricing (shipping, taxes, discounts).
express.on('session_start', ({ wallet, updateWith }) => {
updateWith({
amount: 3498,
shippingOptions: [
{ id: 'standard', label: 'Standard', amount: 499 },
],
lineItems: [
{ label: 'Product', amount: 2999 },
{ label: 'Shipping', amount: 499 },
],
});
});
If you do not register a
session_startlistener, the sheet opens immediately using the initialamountandshippingOptionsyou configured.
shipping_address_change
Fired when the customer selects or changes their shipping address in the wallet sheet. Call updateWith() to provide updated shipping options and pricing for that address.
express.on('shipping_address_change', ({ address, updateWith }) => {
const options = getShippingOptionsForCountry(address.country);
updateWith({
amount: 2999 + options[0].amount,
shippingOptions: options,
lineItems: [
{ label: 'Product', amount: 2999 },
{ label: 'Shipping', amount: options[0].amount },
],
});
});
To reject an unsupported address without closing the sheet:
updateWith({ status: 'invalid_shipping_address' });
shipping_option_change
Fired when the customer selects a different shipping option. Call updateWith() with the updated total.
express.on('shipping_option_change', ({ shippingOption, updateWith }) => {
updateWith({
amount: 2999 + shippingOption.amount,
lineItems: [
{ label: 'Product', amount: 2999 },
{ label: shippingOption.label, amount: shippingOption.amount },
],
});
});
payment_method_created
Fired when the customer confirms payment in the wallet sheet and the SDK has successfully created a Paypercut payment method. This is where you complete the payment.
express.on('payment_method_created', async ({
paymentMethodId,
wallet,
billingDetails,
shippingDetails,
selectedShippingOption,
}) => {
const res = await fetch('/api/confirm-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paymentMethodId,
shippingOption: selectedShippingOption,
}),
});
const { status } = await res.json();
if (status === 'succeeded') {
window.location.href = '/order/success';
}
});
| Field | Type | Description |
|---|---|---|
paymentMethodId |
string |
Paypercut payment method ID — pass to your backend to create a Payment Intent |
wallet |
string |
'apple_pay' or 'google_pay' |
billingDetails |
object |
Name, email, phone, and billing address from the wallet |
shippingDetails |
object |
Shipping name and address from the wallet (if requested) |
selectedShippingOption |
object |
The shipping option the customer selected |
cancel
Fired when the customer dismisses the wallet sheet without completing payment.
express.on('cancel', () => {
// restore any UI changes made during session_start
});
error
Fired on unrecoverable SDK errors (e.g. config issues, network failure).
express.on('error', (err) => {
console.error('Express Checkout error:', err.message);
// show a fallback payment option
});
Dynamic Updates with updateWith()
updateWith() is available in session_start, shipping_address_change, and shipping_option_change events. It pushes updated values into the open wallet sheet without closing it.
| Field | Type | Description |
|---|---|---|
amount |
number |
Updated total in minor units |
lineItems |
LineItem[] |
Updated breakdown rows (Apple Pay only) |
shippingOptions |
ShippingOption[] |
Updated shipping option list |
status |
string |
'success' (default), 'fail', 'invalid_shipping_address', 'invalid_shipping_option' |
Backend: Completing the Payment
When payment_method_created fires, send the paymentMethodId to your backend. Your backend then creates a Payment Intent via the Paypercut API:
POST /v1/payment_intents
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"amount": 3498,
"currency": "EUR",
"payment_method": "pm_01KAXYZ...",
"customer": "cus_01KABC...",
"confirm": true
}
On success, the Payment Intent status will be "succeeded". Respond to the SDK result and update your UI accordingly.
Customer requirement: The
paymentMethodIdmust be associated with a Paypercut customer. Create the customer viaPOST /v1/customersand store thecustomer.idbefore initiating Express Checkout. Pass it in the Payment Intent request.
Instance Methods
After calling PaypercutExpressCheckout(options), you get an instance with the following methods:
| Method | Description |
|---|---|
mount(container) |
Mount wallet buttons into a CSS selector string or HTMLElement |
on(event, handler) |
Subscribe to an event. Returns an unsubscribe function |
setLayout(layout) |
Switch between 'row' and 'column' layouts dynamically |
setLocale(locale) |
Change the wallet button language after mount |
setButtonOptions(opts) |
Update button appearance after mount (merged with existing options) |
destroy() |
Remove buttons, clean up iframes and all event listeners |
Full Example
import { PaypercutExpressCheckout } from '@paypercut/checkout-js';
const SHIPPING_OPTIONS = [
{ id: 'standard', label: 'Standard Shipping', detail: '3–5 days', amount: 499 },
{ id: 'express', label: 'Express Shipping', detail: 'Next day', amount: 999 },
];
const express = PaypercutExpressCheckout({
publishableKey: 'pk_live_...',
amount: 2999,
currency: 'EUR',
countryCode: 'DE',
requestShipping: true,
shippingOptions: SHIPPING_OPTIONS,
lineItems: [
{ label: 'Premium Plan', amount: 2999 },
{ label: 'Shipping', amount: 0, type: 'pending' },
],
layout: 'auto',
locale: 'en',
buttonOptions: {
type: 'buy',
applePayStyle: 'black',
googlePayColor: 'black',
borderRadius: '0.5rem',
},
});
express.on('ready', () => {
document.getElementById('wallet-loading').remove();
});
express.on('session_start', ({ updateWith }) => {
updateWith({
amount: 2999 + SHIPPING_OPTIONS[0].amount,
shippingOptions: SHIPPING_OPTIONS,
lineItems: [
{ label: 'Premium Plan', amount: 2999 },
{ label: SHIPPING_OPTIONS[0].label, amount: SHIPPING_OPTIONS[0].amount },
],
});
});
express.on('shipping_address_change', ({ address, updateWith }) => {
// Recalculate shipping for the selected country
const options = address.country === 'DE' ? SHIPPING_OPTIONS : [
{ id: 'intl', label: 'International', detail: '7–14 days', amount: 1499 },
];
updateWith({
amount: 2999 + options[0].amount,
shippingOptions: options,
lineItems: [
{ label: 'Premium Plan', amount: 2999 },
{ label: options[0].label, amount: options[0].amount },
],
});
});
express.on('shipping_option_change', ({ shippingOption, updateWith }) => {
updateWith({
amount: 2999 + shippingOption.amount,
lineItems: [
{ label: 'Premium Plan', amount: 2999 },
{ label: shippingOption.label, amount: shippingOption.amount },
],
});
});
express.on('payment_method_created', async ({ paymentMethodId, selectedShippingOption }) => {
const res = await fetch('/api/confirm-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentMethodId, selectedShippingOption }),
});
const { status } = await res.json();
if (status === 'succeeded') {
window.location.href = '/order/success';
}
});
express.on('cancel', () => {
console.log('Customer cancelled the wallet sheet');
});
express.on('error', (err) => {
console.error('Express Checkout error:', err.message);
});
express.mount('#express-checkout');
Testing
Use sandbox API keys (pk_test_... / sk_test_...) during development. The wallet buttons will work in sandbox mode with test cards provided by Apple and Google.
For test cards and sandbox scenarios, see the Testing Guide.
Support & Resources
- Paypercut Dashboard
- API Reference — Payment Intents
- API Reference — Customers
- Apple Pay Setup Guide
- Technical Support
Ready to add Express Checkout to your product? Create your free Paypercut account →

