1. Publishable key: identifies the store
- What it does: tells the API which store. Like Stripe’s publishable keys, it can live in the client (web/mobile).
- What it does NOT do: it does not authenticate a user and is rejected on the
/admin/*endpoints (403api_key.admin_route_forbidden). The blast radius of a leaked key is limited to the storefront. - Format:
cx_pk_<live|test>_<slug>_<random>. Usetestonly in development. - It can have origin restrictions and can be rotated or revoked by the operator. A wrong or
revoked key: 401
api_key.invalid; an origin that is not allowed: 403api_key.origin_not_allowed.
2. Customer session: shopper account
For account areas (orders, addresses, wishlist, subscriptions) the customer logs in and gets a token pair:
Registration and login:
POST /auth/register, POST /auth/login (with 2FA if enabled),
POST /auth/refresh, POST /auth/logout, Google and Apple social login when the store has
enabled them (GET /auth/social-providers). See the API Reference.
Customer accounts belong to a store: the same email can have an account in different
stores. Registration and login must therefore happen in the store’s context, that is from
its domain or with the
X-Vellaro-Key header.Rotating refresh
Every login opens a session. On everyPOST /auth/refresh the refresh token you present
is superseded and the response carries a new one: always store it in place of the old
one, together with the new access token.
Refresh tokens issued before the October 10, 2026 release are valid once: on first use
they open a new session and move to the new scheme, so the release signs nobody out. Each
account keeps at most 20 sessions: beyond that, the oldest one is closed.
In the client:
- On a 401 from any call, call
POST /auth/refresh, store the new pair, and retry the request. - Run one refresh at a time: requests that get a 401 at the same moment wait for the same refresh.
- On a 401 from
/auth/refresh(auth.session_revoked,auth.refresh_token_reused,auth.refresh_token_invalid) or on a 403auth.tenant_unavailable, clear the tokens and send the user to the login screen. - On network errors or 5xx, do not drop the session: try again later.
Logout
POST /auth/logoutwith{"refresh_token": "..."}in the body closes this session: from then on that refresh token answers 401auth.session_revoked. It’s idempotent and doesn’t need the access token. The access token stays valid until it expires: drop it in the client.POST /auth/logout?all=truecloses all the account’s sessions, on every device (“sign out everywhere”). It needs a valid credential: theAuthorization: Bearer <access_token>header, or the refresh token in the body if the access token has already expired. Without one: 401auth.not_authenticated.
auth.session_revoked: back to
login. Sessions are also revoked by a password reset, a password change (see the note below),
enabling and disabling 2FA, and the store deactivating the account.
Calls that revoke sessions from the user’s device return new tokens for that device:
adopt them right away, otherwise the next call gets a 401
auth.session_revoked. This applies
to POST /auth/change-password (always revokes), to POST /auth/2fa/verify on first
activation, and to POST /auth/2fa/disable. POST /users/me/change-password revokes and
returns tokens only if you send the X-Session-Rotation: 1 header; without it, it answers as
before and closes no session.Two-factor authentication (2FA)
- Activation:
POST /auth/2fa/setupwith{"current_password": "..."}returnssecretandqr_urito show as a QR code. ThenPOST /auth/2fa/verifywith{"code": "123456", "current_password": "..."}enables 2FA, closes the other sessions, and answers with new tokens plus 10 recovery codes (recovery_codes). - Recovery codes: they look like
xxxxx-xxxxx, are valid once, and must be shown to the user only once, in the activation response. Each one replaces the app code at login and to disable 2FA. - Login: with 2FA enabled, the first
POST /auth/loginanswers 401auth.totp_required. Repeat the same call withtotp_code, which accepts the app code or a recovery code. Each TOTP code is valid once: the same code presented again gives 401auth.totp_invalid. - Regeneration:
POST /auth/2fa/recovery-codeswith{"current_password": "..."}returns 10 new codes; the old ones stop working.GET /auth/meexposesrecovery_codes_remaining(nullwhile 2FA is off): when it gets close to 0, prompt the user to regenerate them. - Deactivation:
POST /auth/2fa/disablewith{"code": "..."}(app code or recovery code). It closes the other sessions, clears the recovery codes, and returns new tokens.
GET /auth/2fa/setup is deprecated: it no longer creates a secret. Typical 2FA errors:
400 auth.current_password_wrong, auth.no_password_set (social-only account),
auth.totp_already_enabled, auth.totp_not_enabled, auth.totp_invalid.
Deleted store
If the account’s store no longer exists:POST /auth/login(with the right password) andPOST /auth/refreshanswer 403auth.tenant_unavailable;- any other authenticated call with a token of that store answers 401
auth.tenant_unavailable.
3. Staff token: Admin API
The/admin/* endpoints require a staff account (JWT) with the right permissions
(catalog, sales, customers, marketing, content, reviews, reports, tax, shipping, system,
users).
- Login is the same
POST /auth/login, with a staff user’s credentials. The same rotation rules apply, with the strict reuse rule described above. - The token carries the
tenant_idclaim: every session is bound to its store (leak-proof multi-tenant isolation). - For automations and long-running agents there is the API token: it’s used as a Bearer, with no login or refresh, and has a mandatory expiry. See Manage your store via API.
- Admin mutations are audit-logged.
- Publishable keys never grant access here.
Common auth error codes
The
/auth/* endpoints have per-IP limits (login 10 per minute, refresh 30 per minute, 2FA
and password change 5 or 10 per minute). Beyond the limit the response is a 429, without
the standard envelope: {"detail": "Rate limit exceeded: ..."}.