Authentication
Clione has two authentication modes, each used for different endpoints.
1. JWT (dashboard sessions)
Issued after a user logs in. Sent as httpOnly cookies, and accepted as an Authorization: Bearer <token> header for cross-origin clients.
POST /api/v1/auth/login— email + password → access + refresh tokensPOST /api/v1/auth/refresh— exchange the refresh token for a new pair; the old refresh token is blacklistedPOST /api/v1/auth/logout— blacklists the current access and refresh tokensGET /api/v1/auth/me— current userPOST /api/v1/auth/google— sign in with a Google ID token
Protection on login:
- Account lockout keyed on client IP + account: after 5 failed attempts the pair is locked for 1 minute, and every further lock of the same pair doubles the wait, up to 60 minutes.
- Rate limit of 10 failed login attempts per IP every 15 minutes.
JWT sessions are for the dashboard. Do not use them for storefront integrations — use an API key.
2FA flow
When a user with TOTP or email 2FA enabled logs in from a device that is not trusted, the login response is:
{ "requires2FA": true, "tempToken": "...", "methods": ["totp", "email", "recovery"] }
methods lists what the user can answer with: totp only when an authenticator app is configured, email always, and recovery while the user has unused recovery codes.
The client then calls POST /api/v1/auth/2fa/validate:
{
"tempToken": "<from login>",
"token": "<6-digit code or recovery code>",
"method": "totp",
"deviceFingerprint": "<optional — if present, the device is trusted for 30 days>"
}
A valid code returns the full session (accessToken, refreshToken, user, tenants) and sets the cookies.
All 2FA endpoints live under /api/v1/auth:
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /2fa/setup | JWT | Generate a TOTP secret; returns secret, qrCodeDataUrl, otpauthUrl |
POST | /2fa/verify-setup | JWT | Enable TOTP with { secret, token } after the user scans the QR code |
POST | /2fa/email/send-code | JWT | Email a code to the signed-in user, to enable email 2FA |
POST | /2fa/email/enable | JWT | Enable email 2FA with { token } |
POST | /2fa/disable | JWT | Disable one method with { token, method } (totp or email). When no method is left, trusted devices and recovery codes are deleted |
GET | /2fa/recovery-codes/status | JWT | { remaining } unused recovery codes |
POST | /2fa/recovery-codes/regenerate | JWT | Issue a new batch with { password } |
POST | /2fa/email-code | tempToken | Email a 6-digit code during login, when the user has no authenticator at hand |
POST | /2fa/validate | tempToken | Validate a totp, email or recovery code and exchange tempToken for a full session |
GET | /2fa/trusted-devices | JWT | List the user's trusted devices (id, userAgent, trustedUntil, lastSeenAt, createdAt, fingerprintShort) |
DELETE | /2fa/trusted-devices/:id | JWT | Revoke one trusted device (404 if it isn't yours) |
Enabling the first 2FA method returns a batch of recoveryCodes once. Email codes expire after 10 minutes.
2. API keys (storefront + server-to-server)
Keys look like sk_live_<random>. The API recognises the sk_live_ and sk_test_ prefixes.
Keys are created by an owner or admin, from Store Settings → API Keys in the dashboard, or via:
POST /api/v1/dashboard/org/api-keys
Authorization: Bearer <JWT>
Content-Type: application/json
{
"name": "Headless storefront — shop.example.com",
"scopes": ["products:read"],
"allowedDomains": ["shop.example.com", "*.example.com"],
"kind": "server",
"tenantPlatformId": "<store id>"
}
The response (201) carries the full key in key. It is shown only once. After that you only see its prefix.
tenantPlatformId is optional: with it the key works for that one store only; without it the key works for every store in the organization.
kind is browser (default) or server — see Key kinds below.
Related endpoints (JWT, owner or admin for writes):
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/dashboard/org/api-keys | List every key in the organization, with its store, scopes, kind and last use |
PATCH | /api/v1/dashboard/org/api-keys/:id | Change active, allowedDomains or name. Scopes cannot be changed: create a new key instead |
DELETE | /api/v1/dashboard/org/api-keys/:id | Delete the key |
Using the key
Send it as a header:
Authorization: Bearer sk_live_...
or:
X-API-Key: sk_live_...
Scopes
The scopes you can put on a key:
| Scope | What it opens |
|---|---|
products:read | Read products, categories, collections, pages and FAQs, and the public storefront endpoints (SEO signals, schema graph, FAQ widget) |
products:write | Trigger syncs through the API |
promotions:read | Read access to the promotions engine |
mcp:public | The store's read-only MCP endpoint, for AI agents |
A request missing a required scope gets 403 with "message": "Insufficient scopes" and the required list. Every endpoint except the promotions ones also returns the missing list.
Domain whitelist
allowedDomains restricts which storefront hosts may use the key on the FAQ widget (/public/faq) and SEO signals endpoints. Patterns are exact hosts (shop.example.com) or wildcards (*.example.com, which also matches example.com). A request whose host is not on the list gets 403.
A key with an empty list accepts any origin, and a browser key with an empty list also skips the browser-context check below. Set the list on every key that ships in a storefront.
Key kinds: browser vs server
kind | Who holds it | What the API checks |
|---|---|---|
browser (default) | Your storefront page — embed.js, the FAQ widget, the Schema Injector. Anyone can read it in the page source. | On the FAQ widget and SEO signals endpoints, Origin/Referer must be on allowedDomains, and, when that list is not empty, the request must come from a real browser (Sec-Fetch-* headers + a Mozilla User-Agent). A plain curl or fetch from Node is rejected with 403. |
server | Your server — a Hydrogen, Next.js or Nuxt loader, a BFF, a build step. Never shipped to the browser. | No browser-context check. allowedDomains still applies: the server names the storefront it renders for in the X-Clione-Storefront-Host header (a bare host, host:port or a full URL), or forwards the page's Origin/Referer. The host must be on the list; with an allowlist and no declared host the request is 403. |
With @clione/seo, set storefrontHost and the header is sent on every call:
const clione = createClioneSeo({
apiUrl: process.env.CLIONE_API_URL!,
apiKey: process.env.CLIONE_API_KEY!, // a `server` key
platform: 'shopify',
storefrontHost: 'shop.example.com', // → X-Clione-Storefront-Host
})
Raw HTTP:
GET /api/v1/public/seo-signals/shopify/product/12345
Authorization: Bearer sk_live_...
X-Clione-Storefront-Host: shop.example.com
Never use a server key in client-side code: it skips the browser guard on purpose.
Key rotation
- Create a new key with the same scopes, domains and store.
- Update your storefront or server to use it.
- Revoke the old one with
PATCH /api/v1/dashboard/org/api-keys/:id { "active": false }or delete it. Requests using it start returning401.