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

  1. Your frontend creates an Elements Group in payment mode and mounts one Express Checkout Element.
  2. Paypercut checks Apple Pay and Google Pay availability and displays eligible buttons.
  3. The customer opens a wallet and authorizes the payment.
  4. The Element emits confirm with an opaque Payment Method and any requested customer details.
  5. Your frontend sends the Payment Method ID and merchant-owned order ID to your backend.
  6. Your backend validates the order, creates and confirms a custom Checkout Session, and returns its client_secret when client-side continuation may be required.
  7. 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:

  1. Create a Checkout Session with ui_mode=custom.
  2. Store its ID before confirmation.
  3. Confirm it with event.paymentMethod.id and a separate idempotency key.
  4. Return the Checkout Session client_secret only to the untrusted client instance and customer that own the payment attempt.
  5. 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.