---
title: Authentication
description: How Sume Developer API keys work and how to send them safely.
---

Sume Developer API requests are authenticated with workspace-scoped API keys
created in the [API Keys dashboard](https://www.sume.com/dashboard/api-keys).

## Send an API key

Use a server-side environment variable:

```bash
export SUME_API_KEY="sume_live_..."
```

Then send either Bearer auth:

```bash
curl https://api.sume.com/v1/me \
  -H "Authorization: Bearer $SUME_API_KEY"
```

or the API key header:

```bash
curl https://api.sume.com/v1/me \
  -H "x-api-key: $SUME_API_KEY"
```

Both forms are accepted by the current API. Use one consistently in each
integration. The Sume CLI defaults to `x-api-key` and can use Bearer mode when
configured.

**Send exactly one.** A request carrying both `Authorization: Bearer` and
`x-api-key` is rejected with `401 unauthorized` and the message
`Send only one API key credential.` — neither header wins, and the second does
not silently shadow the first. This bites gateways and `fetch` wrappers that add
an `Authorization` header of their own on top of a client that already sends
`x-api-key`; strip one of them rather than relying on a precedence rule that
does not exist.

## Scope

Sume resolves workspace, owner, and API key metadata from the key. Do not put
`workspace_id`, `owner_user_id`, or `user_id` in public API request bodies.

Responses expose key metadata such as id, name, prefix, scopes, and last-used
time, but never the full secret.

Scopes are fixed when a key is created and cannot be added later. Keys created
before a scope existed do not carry it. In particular, `actions:read` and
`actions:write` — required by
[Scheduled via the Actions API](/agents/actions/api-trigger) — are only minted on keys created
after the Actions API-call trigger shipped. An older key returns
`403 insufficient_scope` on every Action run request; create a new key and
rotate to it. The same footgun applies to `formats:read` / `formats:write`: a
pre-Formats key calling a Format is `403 insufficient_scope`, never
`404 format_not_found`. There is no API to patch scopes onto an existing key.

## Server-side proxy pattern

Browser and mobile clients should call your backend. Your backend should attach
the Sume API key.

```ts
export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch("https://api.sume.com/v1/avatar-1.0/generate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify(body),
  });

  return new Response(await response.text(), {
    status: response.status,
    headers: { "Content-Type": "application/json" },
  });
}
```

Validate user input and enforce your own authorization before forwarding
requests to Sume.

## Rotation

Create a replacement key, deploy it to your server, verify `GET /v1/me`, then
revoke the old key from the dashboard. If a key appears in logs or chat history,
rotate it.

## Safety rules

- Keep API keys on trusted servers, CI secret stores, or local developer
  machines.
- Do not place API keys in frontend JavaScript, mobile apps, support tickets, or
  screenshots.
- Treat signed upload and download URLs as temporary secrets.
- Rotate keys from the dashboard if a key is exposed.
- Give agents read-only commands first; require explicit confirmation before
  write or paid generation commands.

## Rate limits

Every API key gets a request budget per minute across all of `/v1`, set by the
subscription plan of the workspace the key belongs to. **Reads and writes have
separate budgets**, so a tight status-poll loop cannot 429 your own submits.

| Plan | Writes per minute | Reads per minute |
|---|---:|---:|
| Free | 120 | 4800 |
| Pro | 300 | 12000 |
| Startup | 600 | 24000 |
| Scale | 1200 | 48000 |
| Enterprise | Contact sales | Contact sales |

A **read** is any `GET` or `HEAD` — polling `status_url`, `events_url`,
`result_url`, listing Formats or runs — plus the two POSTs that submit nothing:
`/v1/generation/admission-preview` and the MCP endpoint itself. Everything else
is a **write**: creating runs, cancelling, uploads. The plan number is the write
number; reads get **forty times** that in their own bucket.

That multiple is sized for agents, not for a human watching one run. An agent
harvest can hold twenty-odd jobs open and poll each of them, which is thousands
of reads a minute against work that costs nothing — so reads are deliberately
cheap and the write budget, the tier a plan actually buys, is left alone.

Enterprise is not self-serve. Until a contracted number is provisioned, an
Enterprise key resolves to the Scale row above.

An MCP tool call spends the write budget for the run it creates, once — not for
the JSON-RPC request that carried it. A `jobs_status` poll over MCP spends no
write budget at all.

Read `ratelimit-remaining` rather than counting requests yourself, and back off
on `retry-after`. The headers describe whichever budget the current request
spent from, and a `429` names it in `error.details.scope` (`read` or `write`).

Request rate is not the same thing as generation capacity. How many generations
run at once is governed separately by your plan's concurrency limit, reported on
the `generation_limits` object; raising your request rate does not raise it.

Every response carries the current state:

| Header | Meaning |
|---|---|
| `ratelimit-limit` | Requests allowed in the current window. |
| `ratelimit-remaining` | Requests left in the current window. |
| `ratelimit-reset` | Seconds until the window resets. |
| `retry-after` | Seconds to wait, sent on `429`. |

Unauthenticated requests are limited per client IP at the Free rate, and their
read bucket is held at **four times** the write rate rather than forty — the
agent-sized read budget is for callers who own the jobs they are polling, and
widening the anonymous bucket would only widen the abuse surface.

The read multiple is a deployment setting
(`SUME_COM_API_RATE_LIMIT_READ_MULTIPLIER`), so a self-hosted or preview
deployment can differ. `ratelimit-limit` on the response is always the
authority for the deployment you are talking to; the table above is the
shipped default.

## Common failures

| Status | Common cause | Next step |
|---|---|---|
| `401` | Missing, malformed, or revoked key. | Check the header and create a new key if needed. |
| `403` | Key is valid but not allowed to access the requested surface. | Check workspace membership and key scope. |
| `429` | Rate limit exceeded for your plan's requests-per-minute budget. | Back off and retry after `retry-after`. |
