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.
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.
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.
Takes no arguments.
Returns{ api_keys: ApiKey[] }
api_keys.create
Create a key. The value is returned once and never again.
Arguments
namestringrequired- What it is for, so a later reader can revoke the right one.
scopesstring[]- Defaults to full access. Give ["mail:send"] to a key that only sends.
Returns{ id, name, key }
api_keys.revoke
Revoke a key. It stops working on the next request, with no grace period.
Arguments
idstringrequired
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.
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.