Sellvik / developers
Concepts

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

CredentialHeaderWhere it livesUsed by
Admin keyAuthorization: Bearer sk_live_…Your server/api/v1/admin/*
Publishable keyX-Sellvik-Key: pk_live_…Browser / mobile/api/v1/store/*
Customer JWTAuthorization: Bearer eyJ…Browser / mobile/api/v1/store/* (customers/me, customer-bound carts)
Cart tokenX-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:

ScopeRoutes
products:readGET /v1/admin/products, GET /v1/admin/products/{id}
products:writePOST/PATCH/DELETE on products, variants, media attachments
categories:readGET /v1/admin/categories*
categories:writePOST/PATCH/DELETE on categories
inventory:read(reserved; not yet enforced)
inventory:writePOST /v1/admin/inventory/adjust
media:writePOST /v1/admin/media/upload, attach/detach on products
orders:read(reserved; admin order endpoints land in v1.1)
customers:read(reserved)
webhooks:readGET /v1/admin/webhooks*
webhooks:writePOST/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:

  1. 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.
  2. The request's Origin is 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.
  3. 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:readGET /v1/store/products*, GET /v1/store/categories*
  • cart:writeGET/POST/PATCH/DELETE /v1/store/cart/*
  • checkout:createPOST /v1/store/checkout
  • customers:authPOST /v1/store/auth/{signup,login,refresh,logout}
  • customers:selfGET/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=none or 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): customer for access tokens, customer-refresh for refresh tokens. Reusing one for the other returns 401 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_key within seconds (no cache).
  • For incident response, revoke first, then investigate. The prefix field on every audit log entry tells you which key did what.

On this page