> ## Documentation Index
> Fetch the complete documentation index at: /docs/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.

> [!WARNING]
>
> Managed Cloud projects use their included storefront and pinned runtime. The
> published SDK version and this platform source candidate do not establish
> compatibility with a Cloud installation. Verify the deployed API and caller
> pair using the [Cloud release
> results](/docs/guides/crave-cloud-overview.md#developer-package-compatibility).

`@craveup/storefront-sdk@4.0.0` is published under the `development` dist-tag for the scoped public API at `/api/v1/storefront`, with an exact `@craveup/commerce-contracts@4.0.0` dependency. Catalog and cart operations carry explicit menu, channel and release context; configured food comes from the server. This guide describes that published pair, including its frontend Clerk customer integration.

> [!WARNING]
>
> API-owned customer authentication is a separate SDK 5.0.0 / contracts 5.0.0
> source candidate that is not yet published or activated. Do not use its new
> authentication methods with the published 4.0.0 pair. Published SDK 2.0.1
> lacks this API's scoped menu/cart context and its customer login/OTP/logout
> and direct payment/result methods target retired routes. Keep those consumers
> blocked until the matching deployed API, frozen installs and caller adoption
> are verified. Development publication does not establish production
> compatibility. Do not substitute an unpublished package or private transport.

## Install and configure

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

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 cart store. Customer
tokens come from the active Clerk session and are never persisted by the app:

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

export function createStorefrontBrowser(input: {
  getAuthToken: () => Promise<string | null>;
  merchantSlug: string;
}) {
  return createStorefrontClient({
    baseUrl: process.env.NEXT_PUBLIC_CRAVEUP_API_URL!,
    sessionStore,
    getAuthToken: input.getAuthToken,
    getMerchantSlug: () => input.merchantSlug,
    defaultTimeoutMs: 10_000,
  });
}
```

Create this client only after Clerk is loaded. Supply `getAuthToken` from the
active Clerk session as `() => getToken({ template: "crave-customer" })` and
resolve `merchantSlug` from the storefront's trusted host or public
configuration. See [Customer identity](/docs/guides/customer-identity.md) for the
framework adapter and activation requirements.

| 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. |
| `getMerchantSlug` | For customer flows | Supplies explicit organization routing for customer requests. |
| `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 identity | Clerk environment + active session | Clerk-managed | Clerk-managed |
| 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](/docs/quickstarts/nextjs.md), [React](/docs/quickstarts/react.md), and [Expo](/docs/quickstarts/mobile.md) 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. Let Clerk clear its session on logout, and clear a receipt capability after terminal display or expiry. Never copy customer JWTs into application storage or 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 outcome = await storefrontBrowser.checkout.submitOrder(
  locationId,
  cartId,
  {
    acceptedCatalogEvidence,
    acceptedDiscountEvidence,
    collectionMode: "ONLINE",
    customerConfirmationRevision,
    quoteFingerprint,
  },
  { idempotencyKey, includeCustomerContext: true, revision: cart.revision },
);

const result = await storefrontBrowser.checkout.getOrderSubmissionOutcome(
  locationId,
  cartId,
  outcome.attemptId,
);
switch (result.status) {
  case "PROCESSING":
  case "CUSTOMER_ACTION_REQUIRED":
    break; // poll with bounded backoff
  case "PAID":
    console.log(result.orderId);
    break;
  case "ACCEPTED":
  case "ACTION_REQUIRED":
    console.log(result.status);
    break;
}
```

The Stripe client secret returned with `CUSTOMER_ACTION_REQUIRED` 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 order submission 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

```tsx theme={null}
const { getToken, isLoaded, isSignedIn } = useAuth();

const storefrontBrowser = useMemo(() => {
  if (!isLoaded || !isSignedIn) return null;
  return createStorefrontBrowser({
    getAuthToken: () => getToken({ template: "crave-customer" }),
    merchantSlug,
  });
}, [getToken, isLoaded, isSignedIn, merchantSlug]);
```

Crave does not expose customer login, OTP, or token-storage endpoints. Use the
environment-specific Customer Clerk application and make customer requests
only after Clerk and the SDK token provider are ready. The SDK obtains the
current short-lived token for each customer request and sends `X-Crave-Merchant-Slug`
from `getMerchantSlug`. Then 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.

### Customer catering quotes

Catering links use an event-specific capability. Pass it only through the declared
`cateringToken` option; never put it in a query parameter or arbitrary headers.
All catering operations return private, non-cacheable responses.

```ts theme={null}
const view = await sdk.catering.get(locationId, eventId, { cateringToken });
const result = await sdk.catering.accept(
  locationId,
  eventId,
  {
    quoteId: view.offer.quoteId,
    eventRevision: view.offer.eventRevision,
    offerFingerprint: view.offer.offerFingerprint,
  },
  { cateringToken, idempotencyKey },
);
// Alternatively, decline the current quote using the event revision:
await sdk.catering.decline(
  locationId,
  eventId,
  {
    quoteId: view.quote.id,
    eventRevision: view.event.revision,
    message: "No thanks",
  },
  { cateringToken, idempotencyKey: declineKey },
);
```

Check `view.actions` before showing acceptance or decline. Only accept a non-null
offer after the customer reviews its exact total. On `CATERING_OFFER_STALE`, refetch
and require a new click; do not automatically accept a different amount. Reuse the
same idempotency key only for the same request. The customer response excludes
restaurant-only notes and contact data, shows current `refundedTotal` values, and
provides receipt links only for paid payments belonging to this event. A refund
does not reopen deposit acceptance.

The OpenAPI operations are `getCateringEvent`, `acceptCateringQuote`, and
`declineCateringQuote`, under `/locations/{locationId}/catering/events/{eventId}`
(with `/accept` or `/decline` for POST). Missing `X-Catering-Token` returns 401;
invalid, expired, revoked or wrong-location links return the same 404 response.

### Customer catering balance

An accepted event returns `balance` with the agreed total, settled captures,
outstanding amount, due date, and a nullable payment offer. After the customer
reviews a non-null offer, pay the whole balance with:

```ts theme={null}
const offer = view.balance?.offer;
if (offer) {
  const result = await sdk.catering.payBalance(
    locationId,
    eventId,
    { quoteId: offer.quoteId, offerFingerprint: offer.offerFingerprint },
    { cateringToken, idempotencyKey: balanceKey },
  );
  if (result.outcome === "checkout") navigate(result.checkoutUrl);
}
```

`payCateringBalance` uses `/balance-payment`; send no amount. A successful
response is `checkout` with `dueNow` and `remaining`, or `no_balance`. Refetch
after checkout to confirm settlement. On a stale offer, refetch and require a
new click. Pending payments, refunds, reconciliation and closed events hold
online collection. Refunds never increase what is owed; `REFUND_IN_REVIEW`
requires staff review. New customer links last until the later of the event's
30-day expiry and 30 local calendar days after the balance-due day. Existing
links are never renewed.

### Catering inquiries from your website

A restaurant that turned on website inquiries accepts catering requests from its own
site. Both calls are anonymous. Show the form only when `inquiryForm` reports
`enabled: true`.

```ts theme={null}
const form = await sdk.catering.inquiryForm(locationId);
if (form.enabled) {
  const { reference, answerBy } = await sdk.catering.submitInquiry(
    locationId,
    submission,
    { idempotencyKey },
  );
}
```

Create one `idempotencyKey` per inquiry and reuse it when retrying that inquiry. A
retry then returns the same receipt instead of sending a second request. The receipt
holds only the six-digit `reference` and the `answerBy` time. A location that is not
accepting inquiries returns 409 `CATERING_INQUIRIES_NOT_ACCEPTED`. Submissions are
limited to five a minute per sender. The OpenAPI operations are
`getCateringInquiryForm` and `submitCateringInquiry`.

### Published catering menu

A restaurant's published catering menu is readable without login, a cart or a
catering link. Only the location ID goes in the request.

```ts theme={null}
const menu = await sdk.catering.getPublishedMenu(locationId);
for (const category of menu.categories) {
  for (const item of category.items) render(item, menu.policies);
}
```

Items, modifier groups and options carry their canonical IDs, the printed price
(`exact` cents, a `range`, or `quote_only`), the price basis and servings as the
restaurant reviewed them. A missing fact is `null`, never zero. `catalogRevision`
names the publication the menu was read from. The response is never cached, so a
menu stops being readable as soon as it is unpublished or catering is switched
off; both answer 404 `CATERING_MENU_UNAVAILABLE`, and an unknown or archived
location answers 404 `NOT_FOUND`. The OpenAPI operation is
`storefrontGetPublishedCateringMenu`.

### Requesting catering cancellation

`client.catering.requestCancellation(locationId, eventId, { quoteId, eventRevision, reason }, { cateringToken, idempotencyKey })` records a request for restaurant review. It never cancels the service, charges a cancellation fee, or issues a refund. Use the current accepted quote and event revision from the customer response. Read the recorded request with `client.catering.cancellationRequest(locationId, eventId, { cateringToken })`. A revoked, expired or erased link cannot read or replay a request. Keep the same key and payload when retrying; a different key cannot replace an open request.

Staff decisions and refunds are separate operations. Their controls remain unavailable until the corresponding backend and dashboard slices are deployed.


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