Persist an order first, use Stripe online or certified POS-native pay at store, and recover its authoritative outcome.
Checkout uses the same cart capability, revision, and idempotency protections as
other cart mutations. Submitting an order is a write and is never an anonymous
lookup by cart ID.
The published TypeScript SDK is 2.0.1. The order-first methods in this guide
require SDK 3.0.0, which is not yet published. A consumer pinned to 2.0.1
lacks the current scoped menu/cart context, and its createPaymentIntent()
and getOrderResult() methods target retired routes.
Keep direct payment and result recovery blocked until a compatible package is
released. Do not infer payment from a browser redirect or app return.
During the direct-charge cutover, ordinary Stripe payment admission is hard
locked in sandbox and production. submitOrder() returns
STRIPE_DIRECT_CHARGE_CUTOVER_INCOMPLETE; show checkout as temporarily
unavailable and do not retry or bypass this response. Catalog and cart
development can continue while the lock remains.
Each call uses the latest cart revision. If a call returns CART_CONFLICT,
refresh the cart and require an explicit retry.Guest contact requires a customer name and at least one email address or phone
number. This associates contact with the cart; it does not authenticate the
shopper or claim the guest capability. Sign-in and OTP are not required for
guest checkout.
Render only available server-returned choices. This preflight is not an
admission lease; submission repeats the policy, certification, identity, and
unpaid-exposure checks.
The server persists one canonical order and submission attempt before it creates
the Stripe PaymentIntent. Reusing the same idempotency key recovers the same
attempt; it never creates a second order. Square, Clover, and Toast are not
online processor choices.
When outcome.status is CUSTOMER_ACTION_REQUIRED, initialize Stripe.js with
the returned connectedAccountId and confirm the returned clientSecret:
const { error, paymentIntent } = await stripe.confirmPayment({ elements, confirmParams: { return_url: `${window.location.origin}/checkout/stripe-return`, }, redirect: "if_required",});if (error) throw error;if ( !paymentIntent || !["requires_capture", "succeeded"].includes(paymentIntent.status)) { throw new Error("Stripe payment is not ready for capture");}
Never add the cart capability to return_url. A redirect recovery page may carry
non-secret cart, attempt, and PaymentIntent identifiers, but it must recover the
capability from tab-scoped storage and poll the same persisted attempt before
treating payment as complete.
const result = await storefront.checkout.getOrderSubmissionOutcome( "loc_123", cartId, outcome.attemptId,);switch (result.status) { case "PROCESSING": case "CUSTOMER_ACTION_REQUIRED": // Poll again with bounded backoff or complete the requested Stripe action. break; case "PAID": // Online payment and canonical settlement are complete. break; case "ACCEPTED": // PAY_AT_STORE only: the order exists, but native POS payment is still due. // Read result.verification?.state for PENDING, WARNING, ACTION_REQUIRED, // VERIFIED, CONCLUSIVE_FAILURE, or CANCELED counter-verification state. break; case "ACTION_REQUIRED": // Show the retry-safe operator/customer action without creating another order. break;}
Only PAID proves payment. For ACCEPTED, explain where the customer will pay
and continue polling the same attempt. If provider evidence is delayed, show
Verifying payment — do not charge again. Never infer success from a
redirect, provider response, POS order status, or a settlement row whose
submission attempt has not reached its terminal state.
Accept Payments
Integrate Stripe Elements without exposing server secrets.
Order Tracking
Implement bounded result polling and receipts.
Assisted Ordering
Drive one cart from a voice assistant and a browser at once.