Authentication
Every request to https://api.sendops.dev must carry a SendOps API key as a Bearer token. Keys are issued from the dashboard, scoped to one organization, and granted a fixed set of permissions chosen at creation time.
The Bearer header
Authorization: Bearer sk_live_KQX...ZJ7 No other authentication method is supported in v1. There are no session cookies, no signed-URL flow, no HMAC. If the header is missing, malformed, or the key has been revoked, the response is 401 Unauthorized (or 403 when the key is otherwise valid but is missing the required scope).
Key format
Keys are 32 random bytes (Base32-encoded, no padding) prefixed with their environment:
| Environment | Prefix | Example |
|---|---|---|
| Live | sk_live_ | sk_live_KQX5...ZJ7 |
| Test | sk_test_ | sk_test_8GH2...P4Q |
The first 8 characters after the prefix are stored in plaintext on our side so the dashboard and audit log can identify the key without exposing the secret. The secret itself is hashed (SHA-256) before storage — once you create a key, we can never show it to you again. If you lose it, you must rotate it.
Treat keys like passwords
Anyone with the raw key can call the API on your behalf. Store keys in your secrets manager, never in version control. If a key leaks, revoke it immediately from the dashboard.
Live vs. test keys
Both environments share the same data plane and scope vocabulary — there is no separate test sandbox. The difference is rate:
- Live keys run at the full per-org rate limit (default 600 requests/minute).
- Test keys run at 10% of the live rate (60 requests/minute).
All v1 endpoints are read-only, so the only practical difference is the rate ceiling. Use test keys for CI smoke checks and unit tests so noisy automation doesn’t burn your live budget.
Scopes
Scopes are SendOps ACL permission keys. The full catalogue lives at:
curl https://api.sendops.dev/v1/_scopes Scope keys are namespaced under api.* so they cannot be confused with the dashboard’s internal ACL permissions. Every scope you can attach to a key starts with api..
| Scope | What it unlocks |
|---|---|
api.messages.view | GET /v1/messages, /v1/messages/{id}, /v1/messages/{id}/events. Recipient addresses are masked unless the key also holds api.messages.unmask_recipients. |
api.messages.unmask_recipients | Unlocks the recipient= filter on /v1/messages and the full GET /v1/recipients/{email}/messages route (PII-gated) |
api.reports.view | Deliverability, engagement, template-performance reports |
api.suppressions.view | The account suppression list and per-recipient lookup |
api.undeliverable.view | The SendOps-derived undeliverable list (permanent bounces, complaints, rejects) plus operator-cleared tombstones for ?since= polling |
api.channels.view | Channels (SES configuration sets) |
api.templates.view | Email templates |
api.tracking.view | Tracking subdomains and their DNS state |
api.identities.view | SES identities (email and domain) |
api.account.view | Organization snapshot via /v1/account |
A request that hits an endpoint your key does not cover is rejected with 403 Forbidden and an RFC 7807 problem document. See Errors.
Least privilege
Pick the smallest scope set that solves your use case. Pulling deliverability dashboards into a status page only needs api.reports.view; reading a customer’s message history needs api.messages.unmask_recipients and you’ll want to think hard about who that key is given to.
Creating, rotating, and revoking keys
All lifecycle actions happen in the dashboard, in Settings → API Keys.
Create
Click New API Key, give it a name, choose Live or Test, and select scopes. The raw secret is displayed once — copy it now. If you lose it before storing it, immediately revoke this key and create a new one. There is no recovery path.
If your org has enforce 2FA turned on, creating a key requires a fresh MFA challenge.
Edit scopes
You can update the scope set on an existing key without rotating it. The change is applied immediately and recorded in the audit log (api_key.scopes_updated).
Rotate
Rotation issues a new secret while keeping the same name and scope set. Both the old key and the new key continue to work for 24 hours — a hard, non-negotiable grace window. After 24 hours the old key is automatically revoked by a background job.
Use rotation when:
- You suspect a key may have leaked but want zero-downtime cutover.
- You’re rolling keys on a routine schedule.
Rotation does not require MFA — it’s the safe path because the old key still works while you deploy the new one.
Revoke
Revocation is immediate. Once revoked, the key is dead — every subsequent request returns 401. Use this when:
- You know a key has leaked.
- You want to retire a key that is no longer in use.
If your org has enforce 2FA turned on, revocation requires a fresh MFA challenge.
Lifecycle in the audit log
Every key lifecycle event appears in the org audit log:
api_key.created— actor: the user who created itapi_key.scopes_updated— actor: the user who edited; includes added/removed scope diffapi_key.rotated— actor: the user who rotated; emitted on both the old and new keyapi_key.revoked— actor: the user who revoked it (manual)api_key.grace_expired— actor:system; emitted when the 24h rotation grace ends
Request-level activity (which key called which endpoint when) is not in the audit log — that lives in the observability stack (see operational dashboards in your org settings).
Storing keys safely
A few practical rules:
- Put the key in your secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler — anything but a
.envfile in version control). - Inject the secret into your runtime via environment variable, never hard-code it.
- Add the key prefix (
sk_live_orsk_test_) to your repository’s secret-scanning denylist. - Rotate on a cadence appropriate to your blast radius (we recommend at least annually for long-lived service keys, and immediately whenever a teammate with access leaves).