The Admin API supports two authentication methods. Both reach the same endpoints — they differ in where the credential lives and how authorization is decided:
| Secret API key | JWT + cookie |
|---|
| For | Backend integrations, automations, cron jobs | Browser-based admin apps where a human signs in |
| Credential | sk_… key, created in the admin | Short-lived access token + httpOnly refresh cookie |
| Authorization | Scopes attached to the key (read_products, write_orders, …) | The admin user’s roles and permissions |
| Where it runs | Server-side only — never in a browser | Browser (the refresh token never touches your JS) |
Secret API key (server-to-server)
Create a secret key in the admin under Settings → API Keys, grant it the scopes your integration needs, and pass it to the client:
import { createAdminClient } from '@spree/admin-sdk'
const client = createAdminClient({
baseUrl: 'https://your-store.com',
secretKey: process.env.SPREE_SECRET_KEY, // sk_xxx
})
Each key carries the list of scopes granted at creation time. A request to an endpoint the key isn’t scoped for fails with code: 'access_denied' — the error’s details.required_scope names the missing scope (see Querying & Errors).
Secret keys grant back-office access to your store. Never embed them in client-side code, mobile apps, or public repositories — keep them in server-side environment variables or a secrets manager.
JWT + cookie authentication (browser apps)
For browser-based admin tooling, authenticate as an admin user. The flow is designed so that no long-lived credential is ever exposed to JavaScript:
auth.login() returns { token, user } — a short-lived access token you hold in memory.
- The refresh token never appears in JSON. The server sets it as an
HttpOnly cookie scoped to /api/v3/admin/auth, and the SDK sends requests with credentials: 'include' by default, so the cookie flows automatically.
auth.refresh() takes no arguments — it’s driven entirely by the cookie, and rotates it on every call.
auth.logout() revokes the refresh token server-side and clears the cookie.
import { createAdminClient } from '@spree/admin-sdk'
// A cookie-auth app can start with no credentials at all
const client = createAdminClient({ baseUrl: 'https://your-store.com' })
// Sign in — the refresh token lands in the httpOnly cookie
const { token, user } = await client.auth.login({
email: 'admin@example.com',
password: 'password123',
})
client.setToken(token)
// Auto-recover from expired access tokens
client.onUnauthorized(async () => {
const { token: fresh } = await client.auth.refresh()
client.setToken(fresh)
return true // retry the failed request
})
// Sign out — revokes the refresh token server-side
await client.auth.logout()
Bootstrapping a returning session
Because the refresh cookie outlives the in-memory access token, a returning user can be signed back in without re-entering credentials — call refresh() on app load and treat failure as “not signed in”:
try {
const { token } = await client.auth.refresh()
client.setToken(token)
} catch {
// No valid session — show the login form
}
Identity providers
auth.login() also accepts third-party identity-provider payloads when the server has a matching strategy registered:
const { token } = await client.auth.login({ provider: 'auth0', token: idpJwt })
Current user and permissions
After signing in, client.me.get() returns the admin user’s profile together with their serialized permissions — use it to drive what your UI shows:
const { user, permissions } = await client.me.get()
Staff invitations
The auth resource also exposes the unauthenticated invitation-acceptance flow used when onboarding new staff: auth.lookupInvitation(id, token) returns the safe-to-render invitation context (store, role, inviter), and auth.acceptInvitation(id, token, params) accepts it — issuing a JWT and refresh cookie identical to login. Sending invitations is an authenticated operation on client.invitations.
Custom fetch
Pass a custom fetch implementation when you need request interception, proxying, or a non-standard runtime:
const client = createAdminClient({
baseUrl: 'https://your-store.com',
secretKey: process.env.SPREE_SECRET_KEY,
fetch: customFetchImplementation,
})