Documentation

Approvals

The queue of sends held for a person by keys that require approval, and the call that releases or rejects each one.

An API key can be set to require approval. A send from that key is checked like any other, then stored and held instead of sent, and POST /v1/emails answers 202 with status: "pending_approval" and an approval_id. The approval is what a person decides on: approving releases the message, rejecting cancels it.

Approvals expire 72 hours after the send was requested. An expired approval drops out of the list and can no longer be decided, so a forgotten draft never goes out days later. A background job then marks it expired and its message canceled, usually within a minute; a message whose last release the provider refused keeps its failed status. The ideas, and what an agent should do with a held response, are on Approvals.

The approval object

Returned by the list endpoint.

FieldTypeDescription
idstringThe approval id, returned as approval_id by the send.
message_idstringThe held message. It shows as queued in the message log until it is decided or expires, or as failed after a release the provider refused.
api_key_idstringThe key that made the send.
expires_atstring (ISO 8601)When the approval stops being decidable.
created_atstring (ISO 8601)When the send was held.
messageobject | nullThe content to review. null if the message no longer exists.
message.toarray of stringBare recipient addresses.
message.subjectstringThe subject.
message.textstring | nullThe plain-text body.
message.htmlstring | nullThe HTML body.

message does not include from, cc, bcc, reply_to, attachments or the scheduled time. Retrieve the message with GET /v1/emails/{id} for from and scheduled_at.

List pending approvals

GET /v1/approvals · requires emails:read

Every approval in the workspace that is still pending and has not expired, oldest first. The list is not paged: it returns the envelope described in Pagination with next_cursor always null. Every pending approval is returned in data; when there are 500 or more, has_more is also true, even though nothing is left out.

Example

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

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
      "message_id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
      "api_key_id": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c",
      "expires_at": "2026-09-18T12:00:00.000Z",
      "created_at": "2026-09-15T12:00:00.000Z",
      "message": {
        "to": ["ana@example.com"],
        "subject": "Re: Refund request",
        "text": "Hi Ana, I've issued a full refund. It should arrive within five working days.",
        "html": null
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Decide an approval

POST /v1/approvals/{id} · requires emails:send

Approves or rejects one held send. Only one decision can land: two reviewers deciding at the same moment cannot release the same message twice. A second decision after the first has completed gets 409 invalid_state; one that arrives while the first is still sending gets 409 approval_in_progress.

Who can decide. Approvals are decided by a person in the dashboard, or by an API key without guardrails. The call answers 403 forbidden when the credential is an OAuth access token (the credential an MCP client holds, which is an agent's by construction), when it is the key that made the draft, or when it is a key with guardrails of its own (requires_approval, a non-empty allowed_recipients or a daily_send_limit), so an agent cannot release its own mail or another agent's. Any other API key in the workspace with emails:send can decide. decided_by is recorded as given and is not verified.

Rejecting marks the message canceled. The send still counts against the key's daily cap, which is spent when a draft is held.

Approving releases the message. A draft can wait days, so what was true when it was held is checked again first:

  • Suppressions and topic preferences, for every address in to, cc and bcc. An address suppressed or opted out while the draft waited is dropped. If no to address is left, nothing is sent, the message is recorded as rejected, and the approval is still approved; the response has skipped: true and a reason.
  • The plan allowance, counted over the recipients left.

Then the message is released in one of two ways:

  • If the send was made with a scheduled_at that is still in the future, the message is put on the schedule, its status becomes scheduled, and the response carries that scheduled_at. A hold cannot be walked around by scheduling, and approving does not bring the send forward. Suppressions, topic preferences and the plan allowance are checked once more when it comes due.
  • Otherwise it is sent now, and scheduled_at in the response is null. A suspended workspace, a marketing workspace with no postal address and, for marketing mail, the sending domain's warm-up allowance are checked at this point.

The approval is only recorded as approved once the release has worked. If it fails, the call answers with the reason and the approval stays pending, so it can be approved again once the cause is fixed; it still expires at its original time. A message the provider refused is marked failed and a later approval tries it again. The key's allowlist and daily cap are not checked again: they were checked, and the cap spent, when the draft was held.

A released message updates its thread when the provider accepts it, which clears awaiting_reply.

Path parameters

ParameterDescription
idThe approval id.

Body

FieldTypeRequiredDefaultDescription
decision"approve" | "reject"YesnoneWhat to do with the held send.
decided_bystringYesnoneWho authorised the decision, such as a reviewer's email address. 1 to 200 characters. Stored on the approval once the decision completes, and not returned.
reasonstringNononeUp to 500 characters. Stored on the approval and not returned.

Example

curl -X POST https://api.sendraven.ai/v1/approvals/e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "decision": "approve",
    "decided_by": "ana@example.com"
  }'

Response

200 OK

Both decisions answer the same fields.

FieldTypeDescription
idstringThe approval id.
status"approved" | "rejected"The decision recorded.
message_idstringThe held message's id.
scheduled_atstring (ISO 8601) | nullWhen an approved message is due, if it was put on the schedule. null when it was sent now, and on a rejection.
skippedbooleantrue when the approval was recorded but nothing was sent, because no to address was left after suppressions and topic preferences. false otherwise, and on a rejection.
reasonstring | nullWhy nothing was sent when skipped is true, such as suppressed:hard_bounce. null otherwise. It is never the reason you sent in the body.

Approved:

{
  "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
  "status": "approved",
  "message_id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
  "scheduled_at": null,
  "skipped": false,
  "reason": null
}

Approved, but every to address was suppressed while the draft waited, so nothing was sent:

{
  "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
  "status": "approved",
  "message_id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
  "scheduled_at": null,
  "skipped": true,
  "reason": "suppressed:hard_bounce"
}

Rejected:

{
  "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
  "status": "rejected",
  "message_id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
  "scheduled_at": null,
  "skipped": false,
  "reason": null
}

Errors

StatusTypeWhen
402plan_limit_reachedApproving, and the workspace is on Free and its monthly allowance cannot cover the recipients. The approval stays pending.
402payment_method_requiredApproving, and the workspace has no verified payment method. The approval stays pending until a person adds one.
402billing_past_dueApproving, and the paid subscription is unpaid or canceled. The approval stays pending.
402budget_exceededApproving a marketing draft, and the workspace's monthly budget is reached. The approval stays pending.
403forbiddenThe credential is an OAuth access token, the calling key made this draft, or the calling key has a guardrail (requires_approval, allowed_recipients or daily_send_limit).
404not_foundNo approval with this id in the workspace; or approving, and its message no longer exists. The approval stays pending.
409invalid_stateThe approval was already approved or rejected, or it has expired; or approving, and the message is no longer a held draft that can be released (for example it was cancelled).
409approval_in_progressAnother decision is releasing this approval right now. Check the list again in a moment rather than retrying at once.
422invalid_requestThe body failed validation (checked before the approval is looked up).
422no_verified_identityApproving, but the sending domain the message was held on has been removed. Add and verify it again; the approval stays pending.
422workspace_suspendedApproving a message due now, and sending is suspended for this workspace. The approval stays pending.
422no_postal_addressApproving a marketing message due now, and the workspace has no postal address. The approval stays pending.
422warmup_limitApproving a marketing message due now, and its sending domain has used today's warm-up allowance. The approval stays pending.
502ses_errorApproving, and the provider refused or throttled the message. The approval stays pending.

On this page