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

# Storefront SDK

> Use the typed direct client for public catalog, cart, customer, checkout, receipt, and loyalty flows.

`@craveup/storefront-sdk@2.0.1` is the supported TypeScript client for the public API at `/api/v1/storefront`. Version 2 has no browser API-key option and is a deliberate major-version migration from the published 1.x contract.

## Install and configure

```bash theme={null}
pnpm add --save-exact @craveup/storefront-sdk@2.0.1
```

Create separate server and browser clients. A server client has no session or JWT provider; use it only for anonymous published calls:

```ts theme={null}
import { createStorefrontClient } from "@craveup/storefront-sdk";

export const storefrontServer = createStorefrontClient({
  baseUrl: process.env.NEXT_PUBLIC_CRAVEUP_API_URL!,
});
```

A browser client owns the caller-provided, tab-scoped stores:

```ts theme={null}
import { createStorefrontClient } from "@craveup/storefront-sdk";
import { customerTokenStore, sessionStore } from "./storefront-session";

export const storefrontBrowser = createStorefrontClient({
  baseUrl: process.env.NEXT_PUBLIC_CRAVEUP_API_URL!,
  sessionStore,
  getAuthToken: () => customerTokenStore.get(),
  defaultTimeoutMs: 10_000,
});
```

| Option             | Required           | Purpose                                                                   |
| ------------------ | ------------------ | ------------------------------------------------------------------------- |
| `baseUrl`          | Yes                | Exact Crave API origin; remote origins must use HTTPS.                    |
| `sessionStore`     | For carts          | Caller-owned storage for the cart ID, capability, location, and revision. |
| `getAuthToken`     | For customer flows | Supplies the merchant-bound customer JWT.                                 |
| `fetch`            | No                 | Custom fetch implementation for non-browser runtimes.                     |
| `defaultTimeoutMs` | No                 | Timeout for auth-token resolution and the network response.               |

The client ignores caller-supplied authorization credentials and obtains customer auth only from `getAuthToken`. Private API keys are not a Storefront SDK option. The exact typed method surface is the complete public JSON transport API; raw request helpers are not exported, and the navigation-only hosted-storefront redirect is deliberately unwrapped. Do not pass the browser client, its store, or its token provider into a Server Component, server action, or route handler.

Every remote `baseUrl` must use HTTPS. Plain HTTP is accepted only for the exact
loopback hosts `localhost`, `127.0.0.1`, and `[::1]`; a phone-accessible LAN host
such as `192.168.x.x` is not loopback. Redirect responses are rejected rather
than followed. Async `getAuthToken` resolution uses the same timeout as the
network response. `defaultTimeoutMs` and a per-call `timeoutMs` start before the
token lookup, so a provider that does not settle produces a
`StorefrontTimeoutError` without sending a request.

Scope stored data as follows:

| Record             | Required scope                          | Browser persistence        | Mobile persistence  |
| ------------------ | --------------------------------------- | -------------------------- | ------------------- |
| Guest cart session | API environment + merchant + location   | Versioned `sessionStorage` | `expo-secure-store` |
| Customer JWT       | API environment + merchant              | Versioned `sessionStorage` | `expo-secure-store` |
| Receipt capability | API environment + merchant + receipt ID | Versioned `sessionStorage` | Only when resuming  |

Reject malformed or unknown stored versions instead of casting unvalidated JSON. The [Next.js](/quickstarts/nextjs), [React](/quickstarts/react), and [Expo](/quickstarts/mobile) quickstarts provide complete adapters.

Within one client instance, the SDK serializes session-store mutations per
location, including async SecureStore adapters. It persists revisions
monotonically, coalesces identical concurrent ordering-session starts, and
serializes different starts for the same location. Separate client instances or
processes need coordination from the application or storage adapter.

## Authorization model

* Published merchant, location, menu, product, distance, order-time, and gratuity calls are anonymous and never receive the customer JWT.
* `orderingSessions.start` returns a purpose-limited cart capability and saves it through `sessionStore`.
* Cart calls attach the capability, current revision, and an idempotency key where required.
* Customer profile, order, address, saved-payment, and loyalty history calls use the customer JWT.
* Receipt access uses `receiptToken` and sends it only as `X-Receipt-Token`.

Use tab-scoped storage for browser capabilities. Clear the guest capability after claim, deletion, expiry, or terminal checkout handling; after claim, keep only the authoritative cart revision if customer-authenticated access continues. Clear the merchant-scoped JWT on logout and a receipt capability after terminal display or expiry. Never put capabilities or customer JWTs in URLs, public environment variables, logs, source maps, HTML, or analytics.

## Catalog and ordering

```ts theme={null}
const merchant = await storefrontServer.merchant.getBySlug("demo-cafe");
const location = await storefrontServer.locations.getById("loc_123");
const menus = await storefrontServer.menus.list(location.id, {
  menuOnly: true,
});

const readiness = await storefrontServer.locations.getOrderingReadiness(
  location.id,
  "takeout",
);
if (!readiness.ready) throw new Error(readiness.reason);

const { cart } = await storefrontBrowser.orderingSessions.start(location.id, {
  fulfillmentMethod: "takeout",
});

const updated = await storefrontBrowser.cart.addItem(location.id, cart.id, {
  productId: "prod_123",
  quantity: 1,
  selections: [],
  itemUnavailableAction: "remove_item",
});
```

`locations.getOrderingReadiness()` is an anonymous, side-effect-free GET. Use it
to check a fulfillment method without creating a cart; omit the second argument
to use the `takeout` default.

## Checkout and terminal result

```ts theme={null}
const paymentSession = await storefrontBrowser.checkout.createPaymentSession(
  locationId,
  cartId,
  { includeCustomerContext: true },
);

const result = await storefrontBrowser.checkout.getOrderResult(
  locationId,
  cartId,
);
switch (result.state) {
  case "payment_pending":
  case "order_pending":
    break; // poll with bounded backoff
  case "completed":
    console.log(result.order.id);
    break;
  case "failed":
    console.error(result.code);
    break;
}
```

The PaymentIntent client secret is browser-usable but still sensitive: keep it in memory and do not log it or place it in a URL.

Use `includeCustomerContext: true` only when a signed-in shopper still has a
guest capability and payment creation needs Stripe customer association. Omit
it for guest checkout. A claimed cart already uses the customer JWT because its
guest capability has been cleared.

## Customer authentication

```ts theme={null}
const challenge = await storefrontBrowser.customer.login({
  merchantSlug: "demo-cafe",
  identifierString: "alex@example.com",
});

const { token } = await storefrontBrowser.customer.verifyOtp({
  merchantSlug: "demo-cafe",
  identifierString: "alex@example.com",
  customerName: "Alex",
  lastName: "Customer",
  methodId: challenge.methodId,
  otp: "123456",
});
```

After storing the JWT for the current tab, use `customer.getProfile`, `customer.orders`, `customer.addresses`, `customer.savedPayments`, `loyalty.ledger`, and `loyalty.claims`.

## Receipts

```ts theme={null}
const receipt = await storefrontBrowser.receipts.get(receiptId, {
  receiptToken,
});
```

Receipt links place the capability in the URL fragment. Capture it, immediately remove the fragment with `history.replaceState`, and keep it only in tab-scoped storage for that receipt.

## Errors and concurrency

```ts theme={null}
import {
  StorefrontApiError,
  StorefrontClientStateError,
  StorefrontProtocolError,
  StorefrontTimeoutError,
} from "@craveup/storefront-sdk";

try {
  await storefrontBrowser.cart.updateGratuity(locationId, cartId, {
    percentage: "18",
  });
} catch (error) {
  if (error instanceof StorefrontApiError) {
    console.error(error.status, error.code, error.requestId);
    if (error.retryAfterMs !== undefined) scheduleRetry(error.retryAfterMs);
  } else if (error instanceof StorefrontClientStateError) {
    console.error(error.code);
  } else if (error instanceof StorefrontProtocolError) {
    console.error(error.reason, error.method, error.routeTemplate);
  } else if (error instanceof StorefrontTimeoutError) {
    console.error(error.method, error.routeTemplate);
  }
}
```

`StorefrontClientStateError` is a typed local preflight failure. For example,
`CUSTOMER_AUTH_REQUIRED` means a customer-authenticated method was called
without an available JWT, so no network request was sent.
`StorefrontProtocolError.reason` is `EMPTY_RESPONSE` or `INVALID_JSON` for a
successful response that violates the JSON contract. `retryAfterMs` is a
bounded millisecond delay parsed from `Retry-After`; it is `undefined` when the
API did not provide a usable value. On `CART_CONFLICT`, fetch the cart again
before presenting a retry. Reuse an explicit idempotency key when your
application deliberately retries the same logical mutation.

## Navigation-only redirect

`GET /locations/{locationId}/redirect` is a navigation-only REST operation and
is intentionally not part of the typed SDK method surface. Navigate a browser or
native in-app browser to it directly when entering Crave's hosted storefront; do
not fetch it as JSON with the Storefront client. SDK requests reject redirect
responses and never follow them.

## Browser-origin onboarding

Register every exact browser origin with Crave before testing from a browser. The origin includes its scheme, hostname, and any non-default port; local, preview, and production origins are separate registrations. Wildcards are not accepted. Native mobile requests normally omit the browser `Origin` header, but an Expo web deployment still needs registration.
