Skip to main content
The Admin API supports two authentication methods: secret API keys for server-to-server integrations, and JWT bearer tokens for admin SPA / interactive sessions. Every request must include credentials — there is no public surface.
Secret API keys grant full administrative access to your store. Never embed them in client-side code, mobile apps, or public repositories. Use them only from secure server environments.

Secret API key

Pass the key via the X-Spree-Api-Key header:
Secret API keys are prefixed with sk_. Create them with the Spree CLI or in the Spree admin under Settings → API Keys:
Secret keys require at least one scope (see Permissions below); pass --scopes read_all for a read-only key that can access everything.
If you omit the API key, the API returns 401 Unauthorized:

JWT bearer token (admin user)

For interactive admin sessions (the Spree admin SPA, custom dashboards, etc.) authenticate as an admin user and use the returned JWT token for subsequent requests.

Login

Token refresh

Admin JWT tokens are short-lived — they expire after 5 minutes by default (configurable with SPREE_ADMIN_JWT_EXPIRATION, in seconds). Login also sets a refresh token in an HttpOnly cookie scoped to /api/v3/admin/auth; it is never returned in the response body. Exchange that cookie for a new access token:

Permissions

Authorization works differently depending on which credentials you use.

Secret API keys: scopes

Each secret API key carries a list of scopes that grant access to specific resources. Scopes follow a read_<resource> / write_<resource> convention; write_<resource> implies read_<resource>. Custom field values are gated by the resource they’re attached to: a write_products key can manage custom fields on products, variants, and option types; write_orders covers order custom fields, and so on. Custom field definitions (the schema) are part of settings. Exports have no scope of their own. An export is a bulk read, so each export type (/exports/*) is gated by the read scope of the resource it exports — read_customers lets a key create and download customer exports, read_promotions covers coupon-code exports, and so on. The exports list only shows the types the key can read, so a key can never export data it couldn’t read through the API directly. Two scopes are deliberately separate from settings because they’re security-sensitive:
  • webhooks — webhook endpoints receive event payloads (orders, customers) at whatever URL they point to, so the ability to create them is its own grant.
  • api_keys — credential management. A key holding write_api_keys can create new keys, but only with scopes it already holds itself; scopes can never be amplified through the API.
Two convenience aliases:
  • read_all — every read_* scope
  • write_all — every read_* and write_* scope (full admin)
If the key lacks the required scope, the API returns 403 Forbidden:
The details.required_scope field tells you exactly which scope to add — and spree api-key create --type secret --scopes <scope> mints a key that has it. Choose the narrowest set that covers your integration’s needs.

JWT bearer tokens: role permissions

JWT-authenticated admin users pass the same per-controller gate as secret keys. Instead of key scopes, the gate checks the permission keys held by the user’s Spree::Roles on the current store — the same read_<resource> / write_<resource> vocabulary. The SPA reads those keys from GET /api/v3/admin/me to render UI conditionally; partial-permission staff users see only the resources their role grants. If the user’s role lacks the required key, the API returns 403 Forbidden:
A 403 without details.required_permission means the user holds the key but a record-level rule refused the action.

Authentication summary

If both headers are present, the JWT token wins: the user’s role permissions apply and the key’s scopes are ignored. This lets you use sk_xxx to bootstrap a session and then issue per-user JWTs for individual admin actions.

Who a write is attributed to

Records that remember who acted on them — an order’s canceler, approver and created_by, a refund’s refunder, the opener of a return, exchange or claim, the receiver of a stock delivery — name whichever credential made the call. A JWT request records the signed-in admin; a secret-key request records the key itself, so a warehouse connector’s write is attributed to the connector rather than to nobody. Each of these fields comes as a pair. The *_id is a prefixed id, adm_… for a person and key_… for a key, and the *_type beside it says which:
*_type is admin_user or api_key today, and an extension may register further kinds, so treat the value as an open list rather than a closed set. Expanding the association gives the same three facts in one object — id, type and a label fit to show in a timeline:
Attribution answers who, not through which surface. A store upgrading from an earlier release should run rake spree:upgrade:backfill_actor_types so existing records name the kind of actor they already held.