---
title: "Admin SDK authentication with API keys and JWT cookies"
sidebarTitle: "Authentication"
description: "Authenticate @spree/admin-sdk with a secret API key for server-to-server, scope-based access or JWT cookie auth for role-based browser admin apps."
---

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](../../../api-reference/admin-api/authentication.md) 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:

```typescript
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](querying-and-errors.md#scope-errors)).

> **WARNING:** 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.

```typescript
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":

```typescript
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](../../how-to/custom-api-authentication.md):

```typescript
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:

```typescript
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`](resources.md).

## Custom fetch

Pass a custom `fetch` implementation when you need request interception, proxying, or a non-standard runtime:

```typescript
const client = createAdminClient({
  baseUrl: 'https://your-store.com',
  secretKey: process.env.SPREE_SECRET_KEY,
  fetch: customFetchImplementation,
})
```
