# API keys and authentication

One header on every request, and how to scope, store, and rotate the key behind it.

Every request carries Authorization: Bearer esh_..., and nothing else. There is no signing, no client id, and no session. A key belongs to one workspace and can do anything that workspace can do unless you narrow it with scopes.

Any authenticated call:
```bash
curl https://emails.sh/v1/domains \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
```

### Making keys

Create them at https://emails.sh/dashboard/api-keys, or over the API when a deploy pipeline needs its own. The value is in the create response and nowhere else afterwards: we store a hash, so a lost key is replaced rather than recovered.

A key that can only send:
```bash
curl -X POST https://emails.sh/v1/api-keys \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "production web", "scopes": ["mail:send"]}'
```

#### `api_keys.list`

`{ }`

Keys on the workspace, with the last time each was used. Values are never listed.

Returns: { api_keys: ApiKey[] }

#### `api_keys.create`

`{ name: string, scopes?: string[] }`

Create a key. The value is returned once and never again.

| Parameter | Type | Required |
| --- | --- | --- |
| name | `string` | yes |
| scopes | `string[]` | no |

Returns: { id, name, key }

#### `api_keys.revoke`

`{ id: string }`

Revoke a key. It stops working on the next request, with no grace period.

| Parameter | Type | Required |
| --- | --- | --- |
| id | `string` | yes |

Returns: { deleted: id }

### Scopes

| Scope | What it allows |
| --- | --- |
| mail:send | POST /v1/emails and /v1/emails/batch, and reading the status of what it sent. |
| mail:read | Reading received mail at /v1/messages, /v1/threads, and /v1/search. |
| workspace | Domains, webhooks, audiences, and other keys. |

A key with no scopes listed gets all of them. Give the key in your web app mail:send only: an attacker with it can send mail as you, which is bad, but cannot read your inbound mail or repoint your webhooks, which is worse.

### Storing the key

- **Server side only**: A key in browser JavaScript is a key anybody can read. Send from your server, your API route, or your edge function, never from the client.
- **Environment, not source**: Put it in .env, .env.local, or your platform's secret store, and confirm that file is gitignored before you write it.
- **One key per environment**: Separate keys for local, staging, and production means revoking one does not take the others down, and the last-used column tells you which is which.

### Rotating

Create the new key, deploy it, confirm traffic on the new key in the dashboard, then revoke the old one. Revocation takes effect on the next request with no grace period, so doing it in the other order causes an outage.

If a key ever appears in a commit, a log line, or a chat message, revoke it rather than deciding it was probably fine. Revoking takes five seconds and creating a replacement takes five more.

### Rate limits

600 requests a minute per key, which is 10 a second, across every endpoint. The budget is per API key rather than per workspace, so a workspace with several keys gets more, never less. Over it you get 429 with retry-after in seconds. Responses carry x-ratelimit-limit and, where we can compute it cheaply, x-ratelimit-remaining and x-ratelimit-reset.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
