Run the engine
Before a storefront renders anything it needs an engine to talk to. This page gives you one on your laptop, tells you what the demo store in it looks like, and proves the connection with two requests you can paste. Every response on this track was captured from a local engine serving the demo fixture; the public URL shape the pages show, https://api.shop.example/api/v1/..., is what your production storefront will use.
Install in two commands
Section titled “Install in two commands”Follow Install locally. It is one git clone and one wrapper (./setup.sh --local on Linux and macOS, .\setup.ps1 on Windows), takes about seven minutes the first time, and leaves the API on http://localhost:53000, the admin on http://localhost:53200 and the shipped storefront on http://localhost:53300. That page is the reference for the prompts, the containers and the teardown; nothing here repeats it.
If you develop inside the engine repository instead, npx nx serve api listens on the same port 53000.
The demo fixture
Section titled “The demo fixture”The install seeds the demo store, whose definition is test/fixtures/store-configs/demo.config.json. Every value you will meet on this track comes from it:
- Identity: “Demo Store” in English, “Boutique Démo” in French, brand name
demo, a French address in Lyon,businessTypeOnlineStore. - Locales:
en(default) andfr, both left-to-right, timezoneEurope/Paris. - Money: EUR, symbol
€after the amount,,as the decimal separator, a space as the thousands separator, two decimals, prices shown tax included (TTC), 20% VAT. - Checkout: a flat domestic shipping rate of 10,00 € for France, cash on delivery as the only payment method, guest checkout enabled.
- A catalogue with products, categories, copy in both locales and a few seeded customers.
Nothing in the fixture is a secret; it is the shape a real store fills in through the install stepper and the admin.
The base URL and the /api prefix
Section titled “The base URL and the /api prefix”The API mounts every route under a global prefix, api, then a version segment, v1:
- Local:
http://localhost:53000/api/v1/store/config - Production:
https://api.<your-domain>/api/v1/store/config
The pages on this track write https://api.shop.example/api/v1/... for the production shape. Keep the prefix in one constant in your storefront, https://api.shop.example/api, and append /v1/... to it. Two things sit outside it:
GET /healthis served at the root,http://localhost:53000/health, not under/api.GET /api/healthis a 404 (shown below).- The Better Auth routes live under the prefix,
/api/v1/auth/*, and are covered on Auth and session.
The request
Section titled “The request”Two requests prove the engine is up and configured. The first is the liveness probe. The second is the public store configuration, the first call every storefront makes.
curl -s -D - http://localhost:53000/healthcurl -s -D - -H "Accept-Language: en" http://localhost:53000/api/v1/store/configThe same in framework-neutral JavaScript. It runs unchanged in Node 22 and in a browser; credentials: 'include' is what makes a browser send the cookies later pages set, and Node ignores it.
const API = 'https://api.shop.example/api';
const health = await fetch('https://api.shop.example/health');console.log((await health.json()).data.status); // "ok"
const response = await fetch(`${API}/v1/store/config`, { headers: { Accept: 'application/json', 'Accept-Language': 'en' }, credentials: 'include',});const { data } = await response.json();console.log(data.identity.storeName, data.money.currencyCode); // "Demo Store" "EUR"Send Accept-Language on every call. It picks the locale every translatable field is resolved to; the rules are on the conventions page under Translatable text and locale resolution.
The response
Section titled “The response”GET /health, headers and body as the engine sent them:
HTTP/1.1 200 OKCache-Control: no-storeX-Request-Id: 6381a27d-22f2-41f2-be0b-33cca48b9225Content-Type: application/json; charset=utf-8Content-Length: 63{"data":{"status":"ok","timestamp":"2026-09-09T16:01:20.157Z"}}GET /v1/store/config with Accept-Language: en. The headers matter: this is the one route a browser may cache, for sixty seconds, keyed on the locale headers.
HTTP/1.1 200 OKCache-Control: public, max-age=60Vary: X-Locale, Accept-Language, X-Resolve-LocaleX-Request-Id: 54c286f4-4a2e-40ff-8afb-4cb9e62454adContent-Type: application/json; charset=utf-8Content-Length: 2260{ "data": { "identity": { "storeName": "Demo Store", "brandName": "demo", "description": "A demonstration store for themerchantengine.", "legalName": "Demo Store SAS", "contact": { "email": "hello@demo.example", "phone": "+33100000000" }, "address": { "street": "1 Rue de la Démo", "locality": "Lyon", "region": "Auvergne-Rhône-Alpes", "postalCode": "69001", "country": "FR" }, "areaServed": [ "FR" ], "businessType": "OnlineStore" }, "brand": { "baseColors": { "primary": "#1F4E79", "secondary": "#F2B441", "accent": null }, "fontKit": "latin-rounded", "logoAssetId": null, "faviconAssetId": null, "generatedPalette": { "tokens": { "--brand-bg": "#fcfcfd", "--brand-surface": "#f3f4f4", "--brand-surface-elevated": "#ffffff", "--brand-text": "#1a1c1e", "--brand-text-muted": "#656b72", "--brand-text-inverse": "#fcfcfd", "--brand-primary": "#1f4e79", "--brand-primary-hover": "#183c5d", "--brand-primary-active": "#102940", "--brand-on-primary": "#fcfcfd", "--brand-accent": "#f2b441", "--brand-on-accent": "#1a1c1e", "--brand-success": "#1e7f47", "--brand-warning": "#906722", "--brand-error": "#a42f27", "--brand-info": "#276aa4", "--brand-border": "#e4e6e7", "--brand-border-strong": "#858d93", "--brand-border-interactive": "#868d94", "--brand-focus-ring": "#1f4e79", "--brand-surface-strong": "#ecedee", "--brand-on-functional": "#fcfcfd" }, "adjustments": [ { "token": "--brand-accent", "reason": "accent-from-secondary", "from": "#F2B441", "to": "#f2b441" }, { "token": "--brand-success", "reason": "contrast", "from": "#27a45b", "to": "#1e7f47", "against": "--brand-surface", "ratio": 4.56 }, { "token": "--brand-warning", "reason": "contrast", "from": "#a47627", "to": "#906722", "against": "--brand-surface", "ratio": 4.59 }, { "token": "--brand-border-strong", "reason": "contrast", "from": "#b3b8bc", "to": "#858d93", "against": "--brand-surface", "ratio": 3.06 }, { "token": "--brand-border-interactive", "reason": "contrast", "from": "#a3a9ae", "to": "#868d94", "against": "--brand-surface", "ratio": 3.05 } ] } }, "localization": { "supportedLocales": [ "en", "fr" ], "defaultLocale": "en", "rtlLocales": [], "timezone": "Europe/Paris" }, "money": { "currencyCode": "EUR", "symbol": "€", "symbolPosition": "after", "decimalSeparator": ",", "thousandsSeparator": " ", "displayPrecision": 2, "taxDisplay": "TTC", "vatRate": 0.2 }, "domains": { "storefront": "https://demo.example", "admin": "https://admin.demo.example", "api": "https://api.demo.example" } }}Every success on this API is { "data": ... }, with a meta block when the route is a list. What each block of this payload means for your storefront, and how to turn it into CSS, a direction and a price, is the next page: Config, locale and money.
Error codes
Section titled “Error codes”Neither route takes parameters, so the only codes they can answer are the platform ones. The full catalogue is Error codes; the envelope rules are under Response and error envelopes.
NOT_FOUND, 404: the path does not exist. The most common first-day mistake is putting/healthunder the prefix. The storefront shows nothing for this one; it is a build-time bug.RATE_LIMITED, 429:GET /v1/store/configallows 100 requests per minute per client. Cache it (next page) and this never fires from a storefront.
Proof, GET /api/health:
HTTP/1.1 404 Not FoundCache-Control: no-storeX-Request-Id: 583dd8b8-9310-4194-8f3c-32bf57485d3aContent-Type: application/json; charset=utf-8{"error":{"code":"NOT_FOUND","message":"Cannot GET /api/health","details":{"message":"Cannot GET /api/health","error":"Not Found","statusCode":404}}}details is present because the engine ran outside production; a production engine omits it.
Both locales
Section titled “Both locales”GET /health has no locale. GET /v1/store/config does: the same request with Accept-Language: fr answers the same payload with every translatable field switched. Only the identity block differs, shown trimmed to it:
{ "identity": { "storeName": "Boutique Démo", "brandName": "demo", "description": "Une boutique de démonstration pour themerchantengine.", "legalName": "Demo Store SAS", "contact": { "email": "hello@demo.example", "phone": "+33100000000" }, "address": { "street": "1 Rue de la Démo", "locality": "Lyon", "region": "Auvergne-Rhône-Alpes", "postalCode": "69001", "country": "FR" }, "areaServed": ["FR"], "businessType": "OnlineStore" }}brand, localization, money and domains are byte-identical under both locales: the money format is the store’s, not the reader’s.
Framework notes
Section titled “Framework notes”- Keep the base URL in an environment variable your framework reads at SSR (
API_URLin Next.js server code,runtimeConfig.apiUrlin Nuxt,$env/dynamic/privatein SvelteKit) and a second, public one for the browser (NEXT_PUBLIC_API_URL,runtimeConfig.public.apiUrl,$env/dynamic/public). - The two may differ. The server reaches the API over the internal network (
http://api:3000/apiinside a compose stack,http://localhost:53000/apion a laptop); the browser reaches it over the public host (https://api.shop.example/api). The shipped storefront readsSTOREFRONT_API_URLon the server and defaults tohttp://localhost:53000/api. - Never hard-code the prefix per call. One constant,
/v1/...appended, and a mistyped path is a 404 you see once. - The long form for Next.js, Nuxt and SvelteKit is Framework notes.