---
title: Guest checkout
description: Anonymous visitors shop with a signed guest session; the storefront runs guest checkout itself.
sidebar:
  order: 2
---

Guest carts use the Shop API; guest session creation, guest checkout, the private receipt and order lookup run in the
storefront.

Guests have no user token. For cart writes the webshop authenticates as the **storefront** with a client-credentials
token and passes the visitor's guest session alongside it. It calls the API through the typed client
(`callAsGuest((api, options) => api.cart.items.add(input, options), { session: 'required' })`), which sends the token,
`Guest-Session` and `Shopper-IP` as headers. Everything else a guest does runs in the storefront through
`shopOperation`, against the shop data, with the visitor's address from `cf-connecting-ip`.

```mermaid
sequenceDiagram
  autonumber
  participant B as Browser
  participant W as Webshop
  participant TC as Token cache
  participant A0 as Auth0
  participant API as Shop API
  B->>W: Add to cart (fh_guest if present)
  W->>TC: storefront token for SHOP_API?
  alt miss or near expiry
    W->>A0: client_credentials (storefront client)
    A0-->>W: M2M token (1 h)
    W->>TC: store until exp − 60 s
  end
  alt no cart id yet
    W->>W: limit minting by address, sign gs1.<cartId>.<mac>
    W->>B: Set-Cookie fh_guest (HttpOnly, Secure)
  end
  W->>API: POST /v1/cart/items  Bearer <M2M>  Guest-Session  Shopper-IP
  API->>API: client_id = storefront → guest, then verify MAC → browserCartOwner(id)
  API-->>W: Cart
  B->>W: Guest checkout
  W->>W: verify fh_guest, rate limits by cart + visitor address, placeGuestOrder(…)
  W-->>B: Redirect to the private receipt or the bank challenge
```

Guest checkout, bank-challenge completion, private receipt links and number/email lookup run in the storefront. They are
absent from the Shop API and from agent tool discovery.

## Private receipt and bank challenge

Checkout returns the order and its private `accessToken` once. The webshop retains that validated credential in
the HttpOnly `fh_guest_order_<id>` cookie and removes it from the receipt URL. The storefront reads the
order with it directly, so the token never travels in a URL or log of another service. The bank-challenge return page
settles in the storefront with the same capability (the link's token or the cookie); a wrong or missing token is "not
found" and settles nothing.

After a paid or pending challenge, the webshop remembers the token and redirects to `/orders/<id>`.
Failed payments return to checkout. The browser publishable key comes from the webshop.

The number/email lookup (a POST because the email is personal data) uses the domain throttle, `throttleGuestLookup`:
five lookups per client per minute, and five misses per email per hour / fifteen per day across clients. A refusal shows
the private-link message. Lookup returns only receipt status, totals and line titles.

## One signed guest cookie

The webshop reads only `fh_guest`, an HttpOnly, SameSite=Lax, path `/`, thirty-day cookie that is Secure in
production. New carts are minted by the storefront, from server-only code. The storefront verifies the signature on every read, so a
forged or expired cookie is dropped and names no cart. An unsigned `fh_cart` cookie from older builds is ignored and
never recovered or merged. A guest checkout places an order per request; a second one after a paid order finds the cart empty and is refused. Sign-in merges
the cart from the verified Guest-Session and clears the cookie. A rejected capability is discarded; a write can mint
a new one. Ended agent-browser carts render empty and cannot be written.

## Guest rate limits

| Operation                                         | Runs in    | Guest cart budget | Visitor address budget |
| ------------------------------------------------- | ---------- | ----------------- | ---------------------- |
| guest session creation                            | storefront | —                 | 20/minute              |
| `addCartItem`, `updateCartItem`, `removeCartItem` | Shop API   | 60/minute         | 300/minute             |
| guest checkout                                    | storefront | 10/10 minutes     | 30/10 minutes          |
| order lookup                                      | storefront | —                 | 5/minute (+ per email) |

Both Workers keep exact fixed-window counters in the same `rate_limits` D1 table, each under its own keys (the
storefront's minting and checkout limits use `storefront:` keys, the lookup throttle `guest_lookup:` keys). The address is
Cloudflare's connecting IP (an IPv6 address counts as its /64); a missing one uses the shared `unknown` bucket. The Shop
API's 429 `rate_limited` includes `Retry-After` in seconds; the storefront shows the refusal on the page. Customer
callers have no guest rules.

## Security notes

The storefront holds the guest session key, so it can mint a capability for any cart id. That is no worse than its
database binding, which can read any cart, and guessing a 122-bit id remains infeasible. The minting code is imported only
from server-only modules (`check-packages` enforces it), and minting is limited per address. On cart writes the Shop API
trusts the visitor IP header only from the storefront client, and only for rate limiting.

Auth0 meters client-credentials tokens, so the webshop caches the token per isolate and in the Workers cache until 60
seconds before expiry.
