Store API

Build a headless storefront, step by step

Everything you need to go from zero to a working integration: getting a key, authenticating requests, handling the anonymous cart session, signing customers in, and calling your first endpoints. Reference for every field and endpoint lives at /docs/store - this page is the path to get there.

1. Get a key 2. Send it on requests 3. Cart session 4. Register / log in 5. First calls 6. Errors 7. Idempotency 8. Explore live
Using an AI coding agent? Skip the wiring. npx @ordereazi/commerce-agent-toolkit init registers an MCP server in Claude Code, Cursor or VS Code that knows this API's endpoints, schemas and exact per-language SDK method names, and can scaffold a working React storefront against it. You still need a Store Access Key from step 1. The steps below are the same either way - worth reading so you know what your agent built.

1Get a Store Access Key

Every request to /api/v1/store/... needs a key. Keys are created in Backoffice, not via any API call.

  1. In Backoffice, go to Settings → Application APIs (/settings/api).
  2. Click Create Application API.
  3. On the Basics tab, choose the store this key belongs to and set Application Type to Store.
  4. On the Access Control tab, choose a Key Type (see the table below), tick the scopes this integration actually needs, and optionally restrict Allowed Origins to your own domain(s).
  5. Save. The plaintext key is shown once, immediately after creation - copy it now. From then on only a masked preview (sk_store_ab12…xyz9) is shown.

Secret vs. publishable keys

PrefixWhere to use itNotes
sk_store_... Server-side only (your backend) Rejected outright if the request carries a browser Origin header.
pk_store_... Browser or mobile app code Safe to ship in client-side JS - same idea as a Stripe publishable key.

Scopes

Leaving every scope unchecked gives the key full access (anything an anonymous visitor or logged-in customer could do). Tick only what you need:

ScopeGrants
cart:writeAdd, update, or remove items in the cart
checkout:writeComplete checkout and place orders
account:writeManage saved addresses and B2B account members
profile:writeUpdate the profile, change password, or delete the account
wishlists:writeCreate and modify wishlists
gift-registries:writeCreate and modify gift registries

A request that touches a scope the key wasn't granted gets a 403 with code store_key_scope_denied. Read-only calls (browsing, viewing a cart) never require a scope.

2Send the key on every request

Base URL for every Store endpoint: /api/v1/store/...

curl https://your-store.example.com/api/v1/store/catalog/categories \
  -H "X-Commerce-Key: pk_store_yourstore_<secret>"

X-Storefront-Application-Key is also accepted, as a legacy alias for the same header. A missing key returns 401 with code store_key_missing.

Calling from a browser? CORS is open by default - your frontend's Origin is reflected automatically, no allow-list to configure. Only GET, POST, PATCH, DELETE, and OPTIONS are used (no PUT). If you read custom response headers (like X-Session-Ref or X-Total-Count) from JS, note that only those two plus X-Page-Number/X-Page-Size are exposed to browser fetch/XHR - anything else is invisible client-side even though it's on the response.

3Handle the anonymous cart session

A visitor doesn't need to log in to have a cart. The session is tracked with a header, not a cookie.

  • The server generates the session reference on your first request and returns it in the X-Session-Ref response header - you don't invent one.
  • From then on, send that same value back as X-Session-Ref on every subsequent request. That's the entire contract.
  • When the visitor logs in, their anonymous cart carries over onto their account automatically - there's no separate "merge cart" call to make.
curl https://your-store.example.com/api/v1/store/cart \
  -H "X-Commerce-Key: pk_store_yourstore_<secret>" \
  -H "X-Session-Ref: 8f3c1a9d2b7e4f10"

4Register or log in a customer

Only needed for account-gated features - browsing, cart, and checkout as a guest all work without this step.

POST /api/v1/store/auth/login
{ "email": "customer@example.com", "password": "…" }

→ 200 OK
{
  "token": "eyJ…",
  "expiresIn": 3600,
  "sessionReference": "8f3c1a9d2b7e4f10",
  "refreshToken": "…",
  "user": { "id": 1, "email": "customer@example.com", "personId": 42, "accountId": 7 }
}

Send token back as Authorization: Bearer <token> on every request that needs it. It expires after expiresIn seconds (60 minutes) - call POST /api/v1/store/auth/refresh-token with the refreshToken to get a new pair before then.

Refresh tokens rotate on every use. Presenting one you've already used revokes the entire token family (all of that login's tokens), not just that one request - always store and use the newest refreshToken you were given, never retry with an old one.

Cart, catalogue, search, and checkout all work with just X-Session-Ref - no Authorization header required. Orders, saved addresses, wishlists, gift registries, and account/member management require a logged-in customer's Bearer token.

5Make your first calls

A minimal browse → add to cart → check out flow.

# Search the catalogue
curl "https://your-store.example.com/api/v1/store/search/products?keywords=shoes&page=1&limit=18" \
  -H "X-Commerce-Key: pk_store_yourstore_<secret>"

# Add an item to the cart
curl -X POST https://your-store.example.com/api/v1/store/cart/items \
  -H "X-Commerce-Key: pk_store_yourstore_<secret>" \
  -H "X-Session-Ref: 8f3c1a9d2b7e4f10" \
  -H "Idempotency-Key: 3f7a-add-item-1" \
  -H "Content-Type: application/json" \
  -d '{ "productId": 501, "qty": 1 }'

# Place the order
curl -X POST https://your-store.example.com/api/v1/store/checkout/orders \
  -H "X-Commerce-Key: pk_store_yourstore_<secret>" \
  -H "X-Session-Ref: 8f3c1a9d2b7e4f10" \
  -H "Idempotency-Key: 3f7a-place-order-1" \
  -H "Content-Type: application/json" \
  -d '{ }'

Every field, query parameter, and response shape for every endpoint - including the ones skipped here (shipping, payment, gift cards, wallet) - is documented and testable live at /docs/store.

6Handle errors correctly

Expected failures (bad input, not found, unauthorized) come back as RFC 9457 Problem Details:

{
  "type": "https://…",
  "title": "Bad Request",
  "status": 400,
  "code": "quantity_must_be_positive",
  "detail": "Quantity must be greater than 0",
  "errors": null,
  "traceId": "00-abc123…"
}
Business rejections are different - check the success field, not just the status code. Some checkout/cart rules (below minimum order quantity, out of stock, etc.) come back as HTTP 200 with { "success": false, "message": "…" } in the body, since they're not really an error, just an answer the caller needs to react to. Always check success on cart/checkout responses in addition to the HTTP status.

Rate limits

Every key is limited to 200 requests per minute. Login, register, forgot-password, and reset-password are limited more tightly (10 requests per 5 minutes, by caller IP) since they're the credential-guessing surface. Either limit returns 429 with a Retry-After header (seconds to wait).

7Make retries safe with Idempotency-Key

Network retries happen. On any state-changing call (add to cart, place an order, cancel, reorder, and most others), send a client-generated Idempotency-Key header to make a retry safe:

  • Same key + same request again → the original response is replayed, the action does not run twice.
  • Same key + a different request → 422 (you're reusing a key for something else).
  • Same key while the first call is still in flight → 409 with Retry-After: 2.
  • No key sent → the call just runs normally, with no replay protection.

Generate a fresh key per logical action (a UUID is fine) and reuse the exact same value only when retrying that exact same action.

8Explore and test everything live

The full reference - every endpoint, every field, every response shape - is at /docs/store.

Click Authorize at the top of that page, paste in your key, and every "Test Request" button on the page fires a real call using it - no separate Postman setup needed.

Pagination shapes differ per endpoint - always check the endpoint's own docs. For example, product search returns paging info as fields in the JSON body (total, page, pageSize, totalPages), while order listing returns it as response headers (X-Page-Number, X-Page-Size, X-Total-Count) with a plain array as the body. Don't assume one pattern applies everywhere.