> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout Flow

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

> [!WARNING]
>
> 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.

> [!WARNING]
>
> 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.

## 1. Finalize the cart

```ts theme={null}
await storefront.cart.update("loc_123", cartId, {
  fulfillmentMethod: "takeout",
});

await storefront.cart.updateOrderTime("loc_123", cartId, {
  pickupType: "ASAP",
});

await storefront.cart.validateAndUpdateCustomer("loc_123", cartId, {
  customerName: "Alex Johnson",
  emailAddress: "alex@example.com",
  phoneNumber: "+1234567890",
});
```

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.

## 2. Read the current collection options

```ts theme={null}
const choices = await storefront.checkout.getCollectionOptions(
  "loc_123",
  cartId,
  { revision: cart.revision },
);
```

Render only available server-returned choices. This preflight is not an
admission lease; submission repeats the policy, certification, identity, and
unpaid-exposure checks.

## 3. Submit the canonical order

```ts theme={null}
const outcome = await storefront.checkout.submitOrder(
  "loc_123",
  cartId,
  {
    acceptedCatalogEvidence,
    acceptedDiscountEvidence,
    collectionMode: "ONLINE",
    customerConfirmationRevision,
    quoteFingerprint,
  },
  { idempotencyKey: crypto.randomUUID(), revision: cart.revision },
);
```

```bash theme={null}
curl -X POST "https://api.craveup.com/api/v1/storefront/locations/loc_123/carts/cart_456/submissions" \
  -H "X-Cart-Token: $CART_ACCESS_TOKEN" \
  -H 'If-Match: "cart-4"' \
  -H "Idempotency-Key: order_01"
```

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.

## 4. Confirm Stripe customer action

When `outcome.status` is `CUSTOMER_ACTION_REQUIRED`, initialize Stripe.js with
the returned `connectedAccountId` and confirm the returned `clientSecret`:

```tsx theme={null}
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.

## 5. Recover the order outcome

```ts theme={null}
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.

> [card] [Accept Payments](/docs/guides/accept-payments.md)
>
> Integrate Stripe Elements without exposing server secrets.

> [card] [Order Tracking](/docs/guides/order-tracking.md)
>
> Implement bounded result polling and receipts.

> [card] [Assisted Ordering](/docs/guides/assisted-ordering.md)
>
> Drive one cart from a voice assistant and a browser at once.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.