Documentation

API keys

Create, limit, update and revoke a workspace's API keys, including the per-key guardrails meant for keys an agent holds.

These endpoints manage the workspace's sk_live_ API keys, so provisioning can be automated instead of needing a person in the dashboard. They are meant for people and for deployment tooling: a setup script that issues a capped key to a new agent, or a pipeline that rotates a credential. They are deliberately not in /openapi.json, and the MCP server has no tool for them, not even for listing. An agent should be able to send, read and be limited, not rewrite its own limits.

Every endpoint on this page requires domains:write, the closest thing the API has to an administrative scope, so a key that can only send cannot mint a key that can do more. See Authentication for what each scope allows and Limits for agents for how the guardrails behave on a send.

A credential can hand out at most what it holds itself. Creating or editing a key refuses with 403 forbidden any scope the calling key or OAuth token does not hold, and a caller that carries guardrails cannot create or edit a key into something less restricted than itself: the key must have a daily_send_limit no higher than the caller's, an allowed_recipients list inside the caller's, and requires_approval if the caller has it. The details are under Create an API key and Update an API key. A key also may not edit or revoke itself.

Guardrails

A key can carry three limits. They sit on the credential rather than the workspace and are checked on every message the key sends through POST /v1/emails and POST /v1/emails/batch, before anything is stored, because a workspace quota only trips once the damage is done. OAuth access tokens carry none of them.

GuardrailFieldEffect on a send
Daily send limitdaily_send_limitA ceiling on recipients per UTC day, counting every address in to, cc and bcc that will actually be mailed (after suppressed and opted-out addresses are dropped), when a send is held for approval, scheduled, or accepted by SES. A send that would cross it is refused with 429 daily_limit.
Recipient allowlistallowed_recipientsEvery address in to, cc and bcc must match an entry: a full address (ops@example.com) or a domain (example.com or @example.com). Matching ignores case but is otherwise exact, so a subdomain or a lookalike domain does not match. Anything else is refused with 403 recipient_not_allowed. Checked before the daily limit.
Approval holdrequires_approvalEvery send is held for a person to approve instead of going out, and returns status: "pending_approval". Applies to scheduled sends too. See Approvals.

Automations respect the key that starts them: enrolling someone (POST /v1/automations/{id}/enroll) or recording an event (POST /v1/events) is refused with 403 forbidden for a key with requires_approval, and with 403 recipient_not_allowed for an address outside the key's allowlist. The steps an automation sends later do not count against the key's daily limit.

Campaigns respect the key too. A key with requires_approval cannot send, schedule, reschedule or resume a campaign, or decide an A/B test's winner: each answers 403 forbidden. Neither can a key with a recipient allowlist, because an audience cannot be held to one (403 forbidden). A key with a daily limit can start a campaign only when the campaign's recipients fit what is left of the key's limit for the day (otherwise 429 daily_limit), and those recipients count against the limit when the campaign starts. See Campaigns.

A guarded key cannot release mail. Deciding an approval (POST /v1/approvals/{id}) is refused with 403 forbidden when the deciding credential is the key that made the send, or is a key with any guardrail: requires_approval, a non-empty allowed_recipients or a daily_send_limit. An OAuth access token cannot decide approvals either (403 forbidden): approvals are decided by a person in the dashboard, or by an API key without guardrails. See Approvals.

The API key object

The key itself and its hash are never part of the object; the key is returned once, in the create response. Every field is always present: a guardrail the key does not have reads null, [] or false.

FieldTypeDescription
idstringUnique identifier (UUID).
namestringA label for people, 1 to 80 characters.
last4stringThe last four characters of the key, to recognise it by.
scopesarray of stringThe scopes the key holds.
daily_send_limitinteger | nullThe daily send limit. null when the key has none.
allowed_recipientsarray of stringThe recipient allowlist. [] when the key may mail anyone.
requires_approvalbooleantrue when every send is held for approval.
created_atstring (ISO 8601)When the key was created.
last_used_atstring (ISO 8601) | nullWhen the key last authenticated a request. null until it has.
revoked_atstring (ISO 8601) | nullWhen the key was revoked. null on a working key.

List API keys

GET /v1/api-keys · requires domains:write

Returns the workspace's keys newest first, cursor-paged, including revoked keys, which have revoked_at set. See Pagination.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Page size, at most 100. See Pagination.
cursorstringnoneThe next_cursor from the previous page.

Example

curl https://api.sendraven.ai/v1/api-keys \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "8d2f4a6c-1e3b-4c5d-9a7f-0b2d4e6f8a1c",
      "name": "Support agent",
      "last4": "Xk2p",
      "scopes": ["emails:send", "threads:read"],
      "daily_send_limit": 200,
      "allowed_recipients": ["example.com"],
      "requires_approval": true,
      "created_at": "2026-09-15T12:00:00.000Z",
      "last_used_at": "2026-09-15T12:05:00.000Z",
      "revoked_at": null
    },
    {
      "id": "2b4d6f8a-3c5e-4f7a-8b9c-1d3e5f7a9b0c",
      "name": "Old deploy key",
      "last4": "m9Qa",
      "scopes": ["emails:send"],
      "daily_send_limit": null,
      "allowed_recipients": [],
      "requires_approval": false,
      "created_at": "2026-08-01T09:00:00.000Z",
      "last_used_at": "2026-09-10T17:42:00.000Z",
      "revoked_at": "2026-09-12T08:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Create an API key

POST /v1/api-keys · requires domains:write

Mints a key and returns it in key. This is the only time the key is shown: only its SHA-256 hash is stored, so it cannot be read back. Store it before doing anything else with the response. A lost key cannot be recovered; revoke it and create another.

Leaving out a guardrail, passing requires_approval: false, or passing an empty allowed_recipients creates the key without that guardrail. Fields not in the table below are ignored.

The new key is checked against the credential making the request, and refused with 403 forbidden when it would be able to do more:

  • Every scope in scopes must be one the caller holds.
  • If the caller has a daily send limit, daily_send_limit is required and must be no higher than the caller's.
  • If the caller has a recipient allowlist, allowed_recipients is required and every entry must be allowed by the caller's list. An address is allowed when the caller's list has that address or its domain; a domain entry (example.com or @example.com) is allowed only when the caller's list has the same domain.
  • If the caller has requires_approval, the new key must have requires_approval: true.

An OAuth access token carries no guardrails, so only its scopes limit what it can create.

This response is never stored for idempotency, because it holds the only copy of the key. A retry with the same Idempotency-Key after this request has answered mints another key; a retry that arrives while it is still running waits up to 8 seconds for it to finish and then mints another key too, because there is no stored response to return; if it is still running after the wait, the retry answers 409 idempotency_in_progress. If a request times out, list the keys before retrying and revoke any extra one.

Body

FieldTypeRequiredDefaultDescription
namestringYesnone1 to 80 characters.
scopesarray of stringYesnoneAt least one of emails:send, emails:read, domains:read, domains:write, contacts:read, contacts:write, broadcasts:read, broadcasts:write, webhooks:read, webhooks:write, threads:read, templates:read, templates:write.
daily_send_limitintegerNononeA positive integer: the most recipients the key may send to per UTC day.
allowed_recipientsarray of stringNononeUp to 200 entries, each a full address (ops@example.com) or a domain (example.com or @example.com). An entry that is neither, such as acme, ops@ or a URL, is refused with 422, naming it.
requires_approvalbooleanNofalsetrue holds every send from the key for approval.

Example

curl https://api.sendraven.ai/v1/api-keys \
  -X POST \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Support agent",
    "scopes": ["emails:send", "threads:read"],
    "daily_send_limit": 200,
    "allowed_recipients": ["example.com"],
    "requires_approval": true
  }'

Response

201 Created

An API key object with key added.

{
  "id": "8d2f4a6c-1e3b-4c5d-9a7f-0b2d4e6f8a1c",
  "name": "Support agent",
  "last4": "Xk2p",
  "scopes": ["emails:send", "threads:read"],
  "daily_send_limit": 200,
  "allowed_recipients": ["example.com"],
  "requires_approval": true,
  "created_at": "2026-09-15T12:00:00.000Z",
  "last_used_at": null,
  "revoked_at": null,
  "key": "sk_live_T3hYb2VmR2t1c1pBbTlxV0RyNnRaXk2p"
}

Errors

StatusTypeWhen
422invalid_requestname is missing, empty or longer than 80 characters; scopes is missing, empty or contains an unknown scope; daily_send_limit is not a positive integer; allowed_recipients has more than 200 entries; or requires_approval is not a boolean. details lists the problems.
422invalid_requestAn allowed_recipients entry is neither an email address nor a domain. The message starts allowed_recipients: and quotes each such entry, and details gives each one's position, for example ["allowed_recipients", 1].
403forbiddenThe key would hold a scope the caller does not, or would be less restricted than the caller's own guardrails. The message says which.

Update an API key

PATCH /v1/api-keys/{id} · requires domains:write

Changes a key's name, scopes or guardrails in place, so a limit can be tightened without re-issuing the credential and updating everything that holds it. The key itself does not change.

Fields you leave out are unchanged. scopes and allowed_recipients, when given, replace the whole list. To clear a guardrail, send daily_send_limit: null, allowed_recipients: null (or []), or requires_approval: false: the guardrail is removed from the key, and the response reads daily_send_limit: null, allowed_recipients: [] or requires_approval: false.

Unlike create, unknown fields are refused with 422.

A key may not update itself. Raising your own daily limit or lifting your own approval hold would be the guardrail removing itself on the instruction of the thing it guards, so a request whose id is the calling key answers 403 forbidden. Use a different key or the dashboard. A revoked key cannot be updated either, and answers 409 invalid_state.

An update cannot give a key more than the caller holds, by the same rules as Create an API key, applied to the key as it would be after the update:

  • A scope the key does not already have must be one the caller holds. Removing scopes is always allowed, and so is keeping ones the caller lacks.
  • A guardrail the request sets or clears must stay within the caller's own: with a caller limited to 100 a day, daily_send_limit: null and daily_send_limit: 500 are refused and 50 is accepted.
  • When the update adds a scope, the whole resulting key must be within the caller's guardrails, even the fields the request does not mention, because gaining a scope is gaining power.

Changing only name, or only removing scopes, is never refused by these rules.

Path parameters

ParameterDescription
idThe key's id.

Body

FieldTypeRequiredDefaultDescription
namestringNounchanged1 to 80 characters.
scopesarray of stringNounchangedAt least one scope from the list under Create an API key. Replaces the current scopes.
daily_send_limitinteger | nullNounchangedA positive integer sets the limit; null removes it.
allowed_recipientsarray of string | nullNounchangedUp to 200 entries, replacing the current list; null or [] removes the allowlist. Each entry must be a full address or a domain (example.com or @example.com), as on create.
requires_approvalbooleanNounchangedtrue turns the approval hold on; false removes it.

Example

curl https://api.sendraven.ai/v1/api-keys/8d2f4a6c-1e3b-4c5d-9a7f-0b2d4e6f8a1c \
  -X PATCH \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "daily_send_limit": 50, "allowed_recipients": null }'

Response

200 OK

The API key object as stored after the update.

{
  "id": "8d2f4a6c-1e3b-4c5d-9a7f-0b2d4e6f8a1c",
  "name": "Support agent",
  "last4": "Xk2p",
  "scopes": ["emails:send", "threads:read"],
  "daily_send_limit": 50,
  "allowed_recipients": [],
  "requires_approval": true,
  "created_at": "2026-09-15T12:00:00.000Z",
  "last_used_at": "2026-09-15T12:05:00.000Z",
  "revoked_at": null
}

Errors

Checked in this order: the body, then the allowed_recipients entries, then whether the key is the caller, then whether it exists, then whether it is revoked, then what the update would grant.

StatusTypeWhen
422invalid_requestA field is invalid, or the body has a field not in the table above. details lists the problems.
422invalid_requestAn allowed_recipients entry is neither an email address nor a domain. The message starts allowed_recipients: and quotes each such entry. Nothing is changed.
403forbiddenid is the key making the request. A key cannot edit its own scopes or limits.
404not_foundNo key with this id in the workspace.
409invalid_stateThe key is revoked. A revoked key cannot be edited.
403forbiddenThe update would add a scope the caller does not hold, or leave a guardrail less restrictive than the caller's. The message says which.

Revoke an API key

DELETE /v1/api-keys/{id} · requires domains:write

Revokes a key. It stops authenticating at once: every request made with it afterwards answers 401. The key stays in the list with revoked_at set, so recent traffic can still be traced to it. Revoking cannot be undone through the API. Revoking a key that is already revoked succeeds again and keeps its original revoked_at.

A key may not revoke itself, because a request that locked out the only key a script holds would leave no way back through the API. Use a different key or the dashboard.

Path parameters

ParameterDescription
idThe key's id.

Example

curl https://api.sendraven.ai/v1/api-keys/2b4d6f8a-3c5e-4f7a-8b9c-1d3e5f7a9b0c \
  -X DELETE \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

The key object with revoked_at set, the same object the list shows for it from now on. A revoked key is not deleted, so this is the key rather than a deletion result.

{
  "id": "2b4d6f8a-3c5e-4f7a-8b9c-1d3e5f7a9b0c",
  "name": "Old deploy key",
  "last4": "m9Qa",
  "scopes": ["emails:send"],
  "daily_send_limit": null,
  "allowed_recipients": [],
  "requires_approval": false,
  "created_at": "2026-08-01T09:00:00.000Z",
  "last_used_at": "2026-09-10T17:42:00.000Z",
  "revoked_at": "2026-09-15T12:00:00.000Z"
}

Errors

Checked in this order: whether the key exists, then whether it is the caller.

StatusTypeWhen
404not_foundNo key with this id in the workspace.
403forbiddenid is the key making the request. A key cannot revoke itself.

On this page