Authentication
The three credential classes — admin keys, publishable keys, and customer JWTs — and which goes where.
Sellvik authenticates every request with one of three credential classes. Each
exists for a different deployment shape; mixing them up returns 401 or
403, never silent success.
At a glance
| Credential | Header | Where it lives | Used by |
|---|---|---|---|
| Admin key | Authorization: Bearer sk_live_… | Your server | /api/v1/admin/* |
| Publishable key | X-Sellvik-Key: pk_live_… | Browser / mobile | /api/v1/store/* |
| Customer JWT | Authorization: Bearer eyJ… | Browser / mobile | /api/v1/store/* (customers/me, customer-bound carts) |
| Cart token | X-Sellvik-Cart: eyJ… | Browser / mobile | /api/v1/store/cart/*, /checkout |
A single request can carry an admin key or a publishable key (never both), and optionally add a customer JWT and/or cart token alongside the publishable key.
Admin keys (sk_live_…)
Server-to-server credentials. Hashed with SHA-256 before storage; the plaintext is shown once at creation and never recoverable. Format:
sk_live_<shop-shard>_<base64url-secret>The <shop-shard> is the first six chars of the shop UUID — non-secret, used
for triage when a merchant reports a leaked key.
Send as:
Authorization: Bearer sk_live_a1b2c3_AbCdEf...Never expose admin keys in browser code. They carry full CRUD over every
resource within the granted scopes. CORS is intentionally off on
/v1/admin/* — the browser will block requests from another origin, which is
the correct behaviour: the boundary is auth, not CORS.
Scopes
Admin keys carry an immutable scope set chosen at creation. The route enforces the scope:
| Scope | Routes |
|---|---|
products:read | GET /v1/admin/products, GET /v1/admin/products/{id} |
products:write | POST/PATCH/DELETE on products, variants, media attachments |
categories:read | GET /v1/admin/categories* |
categories:write | POST/PATCH/DELETE on categories |
inventory:read | (reserved; not yet enforced) |
inventory:write | POST /v1/admin/inventory/adjust |
media:write | POST /v1/admin/media/upload, attach/detach on products |
orders:read | (reserved; admin order endpoints land in v1.1) |
customers:read | (reserved) |
webhooks:read | GET /v1/admin/webhooks* |
webhooks:write | POST/PATCH/DELETE on webhooks |
Missing the right scope returns 403 insufficient_scope with the required
scope name in the response body. Scope strings are part of the public
contract — they will never be renamed, only added.
Publishable keys (pk_live_…)
Browser-safe credentials. Identify the shop; do not authorise anything a public visitor couldn't already do.
Send as:
X-Sellvik-Key: pk_live_a1b2c3_AbCdEf...Three preconditions for a publishable key to work:
- The shop has headless mode enabled. Toggle in panel → Settings → API
keys. Disabled by default; turning it on opts the shop into supporting
external storefronts. Without it:
403 headless_mode_disabled. - The request's
Originis in the shop's allowed-origin list. Exact string match. No wildcards, no protocol stripping. Without it: the response succeeds at the server but the browser blocks it (CORS). See CORS. - The key has not been revoked. Revocation is immediate; the next call
returns
401 invalid_key.
Publishable capabilities
Publishable keys have no per-call scope. Their capability ceiling is the set
of endpoints exposed under /v1/store/*:
catalog:read—GET /v1/store/products*,GET /v1/store/categories*cart:write—GET/POST/PATCH/DELETE /v1/store/cart/*checkout:create—POST /v1/store/checkoutcustomers:auth—POST /v1/store/auth/{signup,login,refresh,logout}customers:self—GET/PATCH /v1/store/customers/me*reviews:write— (Phase 3)
Customer JWTs
Issued by Sellvik after a successful POST /v1/store/auth/login or
/signup. Use them on customer-bound endpoints (anything under
/v1/store/customers/me*, and writes to a customer-owned cart).
Format and properties:
- Algorithm: HS256 only. Tokens claiming
alg=noneor any other algorithm are rejected. - Issuer (
iss):sellvik. - Audience (
aud): the shop ID. A token for shop A is rejected when presented at shop B. - Kind (
kind):customerfor access tokens,customer-refreshfor refresh tokens. Reusing one for the other returns401 invalid_customer_token. - TTL: access token ~1 hour, refresh token ~30 days.
- Per-shop secret: lazily generated on first use; stored on
Shop.customerJwtSecret. A leaked secret can forge tokens for only that one tenant.
Send as:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Sellvik-Key: pk_live_a1b2c3_...Both headers required: the publishable key tells us which shop; the JWT tells us which customer.
Token refresh
When the access token expires (the API returns 401 invalid_customer_token
with reason: "expired"), exchange the refresh token for a new pair:
curl -X POST https://api.sellvik.app/api/v1/store/auth/refresh \
-H "X-Sellvik-Key: <publishable-key>" \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "<refresh-token>" }'Refresh tokens are single-use. The response includes a new refresh token;
the old one is invalidated. Reusing a refresh token returns 401 with
reason: "replayed", which also revokes the entire token family for that
customer — treat it as a credential-compromise signal.
Cart tokens
Opaque JWT (kind=cart) identifying a guest cart. Issued by POST /v1/store/cart/items for the first time. Send back on subsequent cart calls:
X-Sellvik-Cart: eyJ...When a guest cart is converted to a customer cart (after login), the cart token is still accepted, but the canonical identity becomes the customer JWT.
Rotation and revocation
- Mint a new key in the panel; the old one keeps working until you revoke it.
- Revoke from the panel; the next call returns
401 invalid_keywithin seconds (no cache). - For incident response, revoke first, then investigate. The
prefixfield on every audit log entry tells you which key did what.