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

  1. Your page loads the Paypercut JS SDK and mounts the Express Checkout buttons into a container element.
  2. Apple Pay and Google Pay buttons appear instantly — no checkout session needed upfront.
  3. The customer taps a wallet button. The native payment sheet opens (managed entirely by Apple / Google).
  4. The customer reviews the order, selects their card, billing address, and shipping option, then authenticates with biometrics or device PIN.
  5. The SDK fires a payment_method_created event containing a paymentMethodId.
  6. Your backend uses that paymentMethodId to create a Payment Intent via the Paypercut API and charge the customer.
  7. Paypercut processes the payment and sends the result via webhook.

Important: The paymentMethodId is 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:

  1. A Paypercut merchant accountSign up and obtain your publishable API key from the dashboard under API Keys.

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

  3. 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_start listener, the sheet opens immediately using the initial amount and shippingOptions you 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 paymentMethodId must be associated with a Paypercut customer. Create the customer via POST /v1/customers and store the customer.id before 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


Ready to add Express Checkout to your product? Create your free Paypercut account →