Authentication
The store key, the server secret, registered websites, bearer tokens for customers and merchant integrations, and how to rotate credentials.
Every call names its store with a public key. Calls from your servers also prove where they come from with the store's server secret, and calls from a browser are accepted only from websites you have registered. Customer and merchant routes add a bearer token on top.
The headers
| Header | When | Example |
|---|---|---|
X-Tenant-Key | Every call. It is the only thing that identifies the store. | pk_live_… |
X-Tenant-Secret | Every call from a server. Never from a browser. | your server secret |
Authorization | Customer routes (a token from POST /v1/auth/login) and Merchant API routes (a merchant API key). | Bearer … |
X-Client-IP | Optional on server calls: the shopper's IP address, so rate limits apply per shopper. Trusted only with a valid secret. | 203.0.113.7 |
X-Cart-Token | Cart and checkout routes. | from POST /v1/cart |
X-Order-Access-Token | Viewing a guest's order. | from checkout |
Idempotency-Key | Writes you might retry. See Idempotency keys. | a UUID |
All of these are in Settings → Storefront API in your store admin, apart from customer tokens (which the API issues) and merchant API keys.
Server calls and browser calls
The API tells the two apart by the Origin header, which browsers add and servers do not.
| Server call | Browser call | |
|---|---|---|
| Made by | Your storefront server, a script, an integration | JavaScript on a web page |
| Must carry | X-Tenant-Key and X-Tenant-Secret | X-Tenant-Key only, from a registered website |
| Can use | Every route its credentials allow, and X-Client-IP | The public routes. Routes that need the secret, such as consent, refuse it. |
We recommend calling the API only from your own server, as a backend-for-frontend. The starters all work this way, and refuse to start in production without the secret.
const API = 'https://api.elaraone.app/api/v1'
export async function elaraone(path: string, init: RequestInit = {}, shopperIp?: string) {
return fetch(`${API}${path}`, {
...init,
headers: {
'X-Tenant-Key': process.env.ELARAONE_TENANT_KEY!,
'X-Tenant-Secret': process.env.ELARAONE_TENANT_SECRET!,
...(shopperIp ? { 'X-Client-IP': shopperIp } : {}),
...init.headers,
},
})
}Never forward the browser's headers
Server-side fetch in Node.js sends no Origin. If you copy the shopper's request headers onto your API call, their Origin comes too and turns a server call into a browser call. Build the headers yourself, as above.
Keep the secret out of any environment variable your framework copies into browser code: NEXT_PUBLIC_* in Next.js, NUXT_PUBLIC_* and VITE_* in Nuxt, PUBLIC_* in Astro.
Registered websites
A browser call is accepted only from a website the store has registered in Settings → Storefront API → Your websites. Enter each origin with its scheme and host, such as https://shop.example.com, and no path. Only https:// websites can be registered; register www. and any separate checkout host as well.
If your storefront never calls the API from a browser, switch Browser calls off on the same page. The store then refuses every call without the secret.
CORS is set per store. A response to an accepted browser call echoes your website in Access-Control-Allow-Origin, never *, and credentials (cookies) are never allowed.
The website check is not a password
It stops other websites' JavaScript from using your public key. A program that is not a browser can send any Origin it likes, so browser calls only ever get the public tier. Anything private needs the secret, and so a server.
The API lock
A new store starts in report-only for 14 days: nothing is refused, but every call the lock would refuse is counted and shown in Settings → Storefront API → API lock, by website and by kind. After 14 days the lock turns on by itself. You can turn it on sooner from the same page.
Once the lock is on, these calls are refused with 403:
error.code | Why | Fix |
|---|---|---|
TENANT_SECRET_REQUIRED | A server call without X-Tenant-Secret. | Send the secret on every server call. |
TENANT_SECRET_INVALID | A server call with the wrong secret, or a secret-only route called without it. | Copy the current secret from Settings → Storefront API. |
ORIGIN_NOT_ALLOWED | A browser call from a website the store has not registered. | Register the website, or call from your server. |
BROWSER_CALLS_DISABLED | A browser call while browser calls are switched off. | Call from your server with the secret. |
None of these is worth retrying: the same call gets the same answer until your code or the store's settings change.
Before the lock turns on, check that every website is registered, that every server, job and integration sends the secret, and that the report shows no calls without the secret for the last few days.
Customer tokens
POST /v1/auth/login (or POST /v1/auth/register) returns a bearer token for the customer. Send it as Authorization: Bearer …, together with your store key and secret, on account routes such as GET /v1/auth/me and GET /v1/orders. Keep the token on your server, for example in an httpOnly cookie on your own domain. See Customers and accounts.
Merchant API keys
The Merchant API (/v1/merchant/…) is for back-office integrations: CRM sync, order fulfilment, forms, flows and pages. A merchant API key is sent as a bearer token, alongside the store key and secret, and carries only the abilities it was given:
| Ability | Allows |
|---|---|
crm:read, crm:write | Reading and writing contacts, companies, tasks, notes and associations |
read_orders, manage_orders | Listing orders; dispatching, marking delivered and recording offline refunds |
manage_forms, manage_flows, manage_pages | Forms, automation flows and CMS pages |
manage_consent | The cookie consent banner and consent records |
view_conversations | Inbox conversations |
A key without the ability a route needs gets 403 FORBIDDEN. A missing or revoked key gets 401 UNAUTHENTICATED.
Rotating credentials
The store key. Settings → Storefront API → Rotate store key. The new key keeps the old prefix. The old key keeps working for 7 days, and responses to calls made with it carry X-Tenant-Key-Deprecated: true: log that header, so a forgotten deployment shows up. End overlap now stops the old key at once, for a key that has leaked. After the overlap, the old key gets 404 TENANT_NOT_FOUND.
The server secret. Rotate it when it may have leaked: it was seen in a browser, committed to a repository, or someone who knew it has left. The change is immediate, with no overlap, so update every server's environment and restart. To keep a locked shop working throughout, rotate just before you deploy.
Rotating either asks for your admin password and authenticator code, and is recorded in the activity log.