Skip to content
You are reading the unreleased documentation. No version is released yet, and these pages describe code that is not in a release.

Conformance checklist

Each item below is one obligation of the contract, with the request that proves it and the observation that ticks it. Run them against your own storefront and the engine it talks to; the base is https://api.shop.example/api/v1 in every example, so replace the host with yours. Every response quoted here was read from a local engine serving the demo store (locales en and fr, EUR). Where an item needs a signed-in customer or a filled cart, the flow page linked from it shows how to get there.

  • 1. The store config is read first and rendered from. Send GET /v1/store/config and see a body with the five keys identity, brand, localization, money and domains; on the demo store localization is { "supportedLocales": ["en", "fr"], "defaultLocale": "en", "rtlLocales": [], "timezone": "Europe/Paris" } and money is { "currencyCode": "EUR", "symbol": "€", "symbolPosition": "after", "decimalSeparator": ",", "thousandsSeparator": " ", "displayPrecision": 2, "taxDisplay": "TTC", "vatRate": 0.2 }. Then change the store’s default locale or currency in the admin and reload the storefront: the language of /, the dir attribute and every price follow the answer with no code change. 01 Config, locale and money.

  • 2. Accept-Language is sent on every call. Send GET /v1/products?pageSize=1 with Accept-Language: en, then with Accept-Language: fr, and see data[0].name change while data[0].slug does not. Send GET /v1/store/config with Accept-Language: fr and see identity.storeName come back as "Boutique Démo". In your storefront, open the browser’s network panel on a French page and check that every request to the API carries Accept-Language: fr (and X-Locale: fr if you send it for Safari). 03 Catalog and search.

  • 3. The session cookie and the cart cookie travel. Add an item to an anonymous cart (POST /v1/cart/items, see 04 Cart) and read the response headers: a Set-Cookie for sessionId with Max-Age=2592000 (thirty days), Path=/, HttpOnly and SameSite=Strict (and Secure on a production install). A bare GET /v1/cart with no cookie answers 200 with an empty cart ("id": "", "items": []) and sets nothing. Then in the storefront, add an item, reload the page, and see the item still there: on a server-rendered page that proves the incoming Cookie header was forwarded and the Set-Cookie relayed. Sign in (02 Auth and session) and see a cookie whose name starts with better-auth on the storefront origin; the account page renders on a full reload.

  • 4. 304 is success; no authenticated response is cached. Send GET /v1/store/config with If-None-Match: "abc" and see 200, not 304, with Cache-Control: public, max-age=60 and Vary: X-Locale, Accept-Language, X-Resolve-Locale; send GET /v1/products?pageSize=1 with the same header and see 200 with Cache-Control: no-store and no ETag on either. The API never answers 304 because it emits no ETag, so your storefront’s 304 handling is exercised only by a cache in front of it or by your framework’s fetch layer; read Conventions for why. To tick this item, stage a 304 yourself: point the client at a stub that answers 304 for one call and see the page render with the value it already held rather than an error. Then confirm that no call carrying a Cookie header runs through a shared cache (cache: 'no-store' or the equivalent; see Framework notes).

  • 5. The session is bootstrapped atomically. Send GET /v1/auth/get-session without a cookie and see 200 with the body null; send it with the session cookie and see the user and session object. The item has two shapes, and a storefront ticks the one it is, or both when it renders on the server and hydrates in the browser. In a browser: one get-session at boot, committed together with the user state or not at all. Sign in, then make that call fail transiently (stop the API, or hit the rate limit) and reload: the header still shows the customer, nothing was cleared, and once the API is back the next reload resolves the session again; a page that waits on the boot (the account page) shows neither a flash of the logged-out shell nor a spinner that never ends. On a server: one get-session per request, with the incoming Cookie forwarded, whose failure renders the anonymous page and clears no cookie. Sign in, then make get-session answer a 500 (or stop the API) and request a page with curl and the session cookie: the HTML is the anonymous page, the status is 200, and the response carries no Set-Cookie that expires the session; with the API back, the same request renders the customer. 02 Auth and session.

  • 6. X-Idempotency-Key on order creation, repeated on a retry. Send POST /v1/orders with a JSON body and no key and see 400 with { "error": { "code": "IDEMPOTENCY_KEY_REQUIRED", "message": "X-Idempotency-Key header is required for this endpoint." } }; send it with X-Idempotency-Key: not-a-uuid and see 400 with IDEMPOTENCY_KEY_MALFORMED. With a filled cart and an address, send the same order twice with the same UUID v4 key and see the same data.id and the same data.orderNumber both times, and one order in the account’s list; send the same key with a changed body and see 409 IDEMPOTENCY_KEY_CONFLICT. In the storefront, double-click the place-order button and see one order. 05 Checkout and orders.

  • 7. Every error code is surfaced in every locale. Send GET /v1/products/does-not-exist with Accept-Language: fr and see 404 with { "error": { "code": "PRODUCT_NOT_FOUND", "message": "Product not found" } }: the message is English under fr, so the storefront must translate the code itself. Open the same slug on your French storefront and see a French not-found page; trigger a checkout error (an empty cart, an out-of-stock item) under each locale of the store and see a sentence in that locale, never the API’s message and never an English sentence on a French page. The codes each route can answer are under “Error codes” on every flow page and in Error codes.

  • 8. The SEO and GEO files are proxied from the API. Send GET /v1/sitemap_index.xml and see 200, Content-Type: application/xml; charset=utf-8, Cache-Control: public, max-age=3600; GET /v1/robots.txt answers text/plain; charset=utf-8 with a Sitemap: line that names the storefront host; GET /v1/llms.txt with Accept-Language: fr answers Vary: Accept-Language and opens with # Boutique Démo. Then request /sitemap_index.xml, /sitemap.xml (a 301 to the index), /robots.txt, /llms.txt and /llms-full.txt on your storefront’s root and see the same bodies with the same content types, and the French llms.txt when you send Accept-Language: fr. 08 SEO and GEO.

  • 9. JSON-LD, canonical and hreflang are emitted. Request a product page from your storefront with curl (no JavaScript) and see in the HTML one <link rel="canonical">, one <link rel="alternate" hreflang="..."> per supported locale plus one with hreflang="x-default" pointing at the default locale’s URL, and a <script type="application/ld+json"> whose @type is Product with the name, description and image from GET /v1/products/{slug}; the home page carries Organization and WebSite with the values from identity and domains in the store config. Paste each block into a structured-data validator and see no error. 08 SEO and GEO names which field feeds which tag.

  • 10. The revalidation route is exposed. Send POST to your route (the shipped one is /api/_internal/revalidate) with no X-Revalidate-Secret and see 401; with the right secret and the body { "paths": ["/en/catalog/product/some-slug"], "reason": "product.write", "eventId": "<uuid>" } and see 200 with { "data": { "invalidated": 1, "eventId": "<uuid>", "deduped": false } }; send the identical body again and see "invalidated": 0, "deduped": true. Then set STOREFRONT_REVALIDATE_URL and STOREFRONT_REVALIDATE_SECRET on the engine, rename a product in the admin, and see the storefront page carry the new name on the next request without a redeploy. 10 Revalidation.

Three generated files hold every storefront request so you do not retype them. They are written by npm run docs:generate into docs/site/reference/_generated/ and regenerated whenever the API changes.

The REST Client file, storefront-api.http in that directory, lists every storefront operation in the order of the reference sidebar, one request per block, with a sample body built from the schema and variables at the top for the base URL, the locale and every path parameter the requests use (id, slug, itemId and the rest, each preset to its own name until you fill it). Open it in VS Code with the REST Client extension, or in an IntelliJ IDE, set baseUrl to your engine (http://localhost:53000/api on a local install) and locale to one of the store’s locales, run the sign-in request first so the tool’s cookie jar holds a session (the file declares no cookie variable; the client keeps cookies between requests), then click “Send Request” above any block; the response opens beside it.

The Postman collection, postman-collection.json in the same directory, holds the same requests for both audiences (storefront and admin), one folder per module, with the sample bodies and no token variables: the collection relies on Postman’s cookie jar, which keeps the Better Auth cookie and the cart cookie between calls the way a browser does. Import it, import postman-environment.json beside it as the environment, set baseUrl and locale there, run the storefront sign-in request once so the jar holds the session, and every request in the storefront folders runs as that customer; a request under an admin folder needs a staff sign-in instead.