Pre-charge validation

Run your own checks at the last safe moment before PaymentKit charges them.
View as Markdown

beforeConfirm is an optional hook you pass alongside your other submit options on Apple Pay, Google Pay, Stripe Link, and buy now, pay later. It runs strictly before PaymentKit creates the charge. Reject it and no charge happens.

Why this exists

Wallet sheets only stay open for a few seconds of user activation after the tap. Running your own validation in the tap handler, before submit(), eats into that window — on a slow network your check alone can burn it, and the sheet fails with NotAllowedError or IntegrationError before the shopper ever sees a charge attempt.

beforeConfirm moves your check to the last possible moment instead of before the sheet opens, so it sits outside the activation window entirely. Exactly where that moment falls depends on the rail:

  • Apple Pay, Google Pay, and Stripe Link — after the shopper authorizes in the sheet, right before PaymentKit calls its confirm endpoint:

  • Buy now, pay later (Klarna, Afterpay, Affirm) — the popup opens blank, synchronously on tap, so it’s never blocked. beforeConfirm then runs before the popup is navigated to the provider — before the shopper has seen or approved anything, and before the single call that both starts and confirms the charge:

    Reject it and the popup closes without the provider ever seeing the request.

If you already kick off your validation call at tap time, beforeConfirm can just await that same in-flight promise — it adds close to zero extra latency on top of a check you were already making.

This is a client-side gate, not server-side enforcement. beforeConfirm stops PaymentKit.js from calling its own confirm endpoint when your check fails. It does not stop a charge made by any other means — the confirm endpoints are public, authenticated only by the checkout session’s token. If you need a guarantee that holds even outside our SDK, enforce your check on your own backend as well; beforeConfirm alone does not provide that.

Add the hook

Pass beforeConfirm in options alongside your other submit options:

1paymentKit.submit({
2 fields: {},
3 paymentMethod: 'apple_pay',
4 options: {
5 customerInfo: {
6 first_name: 'Jane',
7 last_name: 'Smith'
8 },
9 beforeConfirm: async () => {
10 const ok = await validateOnYourBackend();
11 return ok ? true : { ok: false, error: 'Please review your details' };
12 }
13 },
14 onSuccess: (result) => {
15 // ...
16 },
17 onError: (errors) => {
18 // A beforeConfirm rejection surfaces here, in the same errors object
19 // as any other payment failure.
20 }
21});

The same option is available on Google Pay and buy now, pay later — attach it in options wherever you already pass customerInfo or other submit options for that method.

Stripe Link doesn’t use submit() at all: pass beforeConfirm in the options you give initStripeLink() instead — see Stripe Link.

Return value

ReturnResult
Nothing (undefined), or trueCharge proceeds
falseCharge is blocked
{ ok: true }Charge proceeds
{ ok: false, error: 'message' }Charge is blocked, error is shown to the shopper
Throws, or the returned promise rejectsCharge is blocked
Doesn’t resolve within the timeoutCharge is blocked

error is shown to the shopper as-is, so write it as customer-facing text. If you don’t supply one, PaymentKit shows a generic validation-failed message.

For Apple Pay, Google Pay, and buy now, pay later, beforeConfirm is passed inside submit()’s options — and submit() types options as unknown, so a misspelled key or an unusual return shape can’t be caught at compile time, whether you load PaymentKit.js from npm or a <script> tag. Stripe Link is the exception: initStripeLink() takes a directly-typed options parameter, so npm/TypeScript merchants do get real compile-time checking there — <script>-tag merchants still don’t, since there’s no TypeScript at all either way. To stay safe regardless of path, use true/false or { ok: boolean, error?: string } exactly as shown above.

Timeout

beforeConfirm has a time budget. If it hasn’t resolved by then, PaymentKit treats it as a rejection and blocks the charge.

  • Default: 10 seconds
  • Maximum: 20 seconds — values above this are clamped, with a console warning
  • Configurable per submit call with beforeConfirmTimeoutMs
1options: {
2 beforeConfirm: async () => { /* ... */ },
3 beforeConfirmTimeoutMs: 15000
4}

The ceiling exists because wallet sheets have their own timeout after authorization — on Apple Pay over Stripe, roughly 30 seconds, and your charge has to complete inside that window too. An unbounded hook would trade one failure (NotAllowedError) for another (a wallet sheet that hangs and then dies on its own). Keep your validation call fast, or start it at tap time and just await it here.

Behavior on rejection

When beforeConfirm blocks the charge:

  • No confirm call is made, and no charge occurs.
  • On Apple Pay, the native sheet is dismissed as a failure rather than left open.
  • The error is delivered through the same onError callback as any other payment failure, keyed by payment method (for example errors.apple_pay).

Without the hook

If you don’t pass beforeConfirm, PaymentKit behaves exactly as it did before this option existed — nothing changes for existing integrations.