Authentication

Search Documentation

Search across all developer documentation

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:

EnvironmentPrefixExample
Livesk_live_sk_live_KQX5...ZJ7
Testsk_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 org’s configured per-minute rate limit — which is unset (unthrottled) by default.
  • Test keys run at 10% of that ceiling — max(1, limit ÷ 10) — once a limit is configured.

See Rate Limiting for the full model. Most v1 endpoints are reads; a few write surfaces exist — contact roster writes, attribute management, and broadcast create/send (gated by their own *.manage scopes), plus append-only activity ingestion (api.activities.write). 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..

ScopeWhat it unlocks
api.account.viewOrganization snapshot via /v1/account — plan, limits, AWS connection state
api.messages.viewGET /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_recipientsUnlocks the recipient= filter on /v1/messages and the full GET /v1/recipients/{email}/messages route (PII-gated)
api.reports.viewDeliverability, engagement, template-performance reports
api.suppressions.viewThe account suppression list and per-recipient lookup
api.undeliverable.viewThe SendOps-derived undeliverable list (permanent bounces, complaints, rejects) plus operator-cleared tombstones for ?since= polling
api.identities.viewSES identities (email and domain)
api.templates.viewEmail templates
api.channels.viewChannels (SES configuration sets)
api.tracking.viewTracking subdomains and their DNS state
api.contacts.viewGET /v1/contacts and /v1/contacts/{email}. Returns emails, attributes, and per-topic subscription preferences — PII-sensitive, grant deliberately
api.contacts.manageCreate, replace, update, and archive contacts (POST/PUT/PATCH/DELETE /v1/contacts, POST /v1/contacts/bulk), with inline static-List membership — a write scope, PII-sensitive
api.topics.viewGET /v1/topics and GET /v1/contact-list — list-level subscription metadata only, no recipient PII
api.topics.manageCreate, edit, and delete subscription-topic definitions (POST /v1/topics, PUT/DELETE /v1/topics/{name}) — a write scope, but definition-only: no recipient PII. Git-backed topics are read-only here (409 conflict)
api.lists.viewGET /v1/lists, /v1/lists/{id}, /v1/lists/{id}/members, and the membership check. Returns member contact ids/emails — PII-sensitive
api.lists.manageCreate, edit, and delete static List definitions (POST /v1/lists, PUT/DELETE /v1/lists/{id}) — a write scope, but definition-only: no member PII. Git-backed Lists are read-only here (409 conflict)
api.segments.viewGET /v1/segments, /v1/segments/{id}, /v1/segments/{id}/members, and the membership check. Returns the predicate plus member contact ids/emails — PII-sensitive
api.broadcasts.viewRead broadcasts, their delivery state, preview, and per-recipient results
api.broadcasts.manageCreate (POST /v1/broadcasts) and send or schedule (POST /v1/broadcasts/{id}/send) broadcasts — a write scope that can send mail to your audience
api.attributes.viewGET /v1/attributes and /v1/attributes/{id} — the org’s custom-attribute registry (name, type, enum values) that Segment predicates reference
api.attributes.manageCreate, edit, and delete attribute definitions and dry-run an edit’s impact (POST/PUT/DELETE /v1/attributes, POST /v1/attributes/{id}/preview). Git-backed definitions are read-only — the repo wins
api.activities.viewGET /v1/activities, /v1/contacts/{id}/activities, and /v1/contacts/{id}/activities/summary. Returns contact emails plus your free-form activity properties — PII-sensitive, grant deliberately
api.activities.writePOST /v1/activities — ingest behavioral signals, resolving or creating stub contacts by email/external_id. Append-only: a write (not manage) scope, since nothing can be edited or deleted through it
api.activities.managePromote, edit, demote, and dry-run activity-property definitions (POST/PUT/DELETE /v1/activity-properties, POST /v1/activity-properties/{id}/preview) — the typed registry that makes named activity properties referenceable in Segment predicates. Git-backed definitions are read-only — the repo wins
api.assets.viewGET /v1/assets and /v1/assets/{id} — the org’s git-sourced image asset library. Read-only, no recipient PII
api.tags.viewGET /v1/tags — the org’s tag/folder vocabulary, plus the ?tag= / untagged= collection filters on /v1/broadcasts and /v1/assets. No recipient PII
api.workflows.viewRead-only access to Drip Workflow definitions, runs, analytics (exits/funnel), the per-run timeline, and POST /v1/workflows/{id}/dry-run. Dry-run is a POST but mutates nothing persistent — it’s a simulation against an existing contact, not a write, which is why it sits under .view rather than .manage

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 it
  • api_key.scopes_updated — actor: the user who edited; includes added/removed scope diff
  • api_key.rotated — actor: the user who rotated; emitted on both the old and new key
  • api_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 .env file in version control).
  • Inject the secret into your runtime via environment variable, never hard-code it.
  • Add the key prefix (sk_live_ or sk_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).