Skip to main content

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 tokens
  • POST /api/v1/auth/refresh — exchange the refresh token for a new pair; the old refresh token is blacklisted
  • POST /api/v1/auth/logout — blacklists the current access and refresh tokens
  • GET /api/v1/auth/me — current user
  • POST /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:

MethodPathAuthPurpose
POST/2fa/setupJWTGenerate a TOTP secret; returns secret, qrCodeDataUrl, otpauthUrl
POST/2fa/verify-setupJWTEnable TOTP with { secret, token } after the user scans the QR code
POST/2fa/email/send-codeJWTEmail a code to the signed-in user, to enable email 2FA
POST/2fa/email/enableJWTEnable email 2FA with { token }
POST/2fa/disableJWTDisable one method with { token, method } (totp or email). When no method is left, trusted devices and recovery codes are deleted
GET/2fa/recovery-codes/statusJWT{ remaining } unused recovery codes
POST/2fa/recovery-codes/regenerateJWTIssue a new batch with { password }
POST/2fa/email-codetempTokenEmail a 6-digit code during login, when the user has no authenticator at hand
POST/2fa/validatetempTokenValidate a totp, email or recovery code and exchange tempToken for a full session
GET/2fa/trusted-devicesJWTList the user's trusted devices (id, userAgent, trustedUntil, lastSeenAt, createdAt, fingerprintShort)
DELETE/2fa/trusted-devices/:idJWTRevoke 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):

MethodPathPurpose
GET/api/v1/dashboard/org/api-keysList every key in the organization, with its store, scopes, kind and last use
PATCH/api/v1/dashboard/org/api-keys/:idChange active, allowedDomains or name. Scopes cannot be changed: create a new key instead
DELETE/api/v1/dashboard/org/api-keys/:idDelete 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:

ScopeWhat it opens
products:readRead products, categories, collections, pages and FAQs, and the public storefront endpoints (SEO signals, schema graph, FAQ widget)
products:writeTrigger syncs through the API
promotions:readRead access to the promotions engine
mcp:publicThe 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​

kindWho holds itWhat 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.
serverYour 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​

  1. Create a new key with the same scopes, domains and store.
  2. Update your storefront or server to use it.
  3. Revoke the old one with PATCH /api/v1/dashboard/org/api-keys/:id { "active": false } or delete it. Requests using it start returning 401.