Approvals
A key that drafts instead of sends, and the queue a person releases from.
An API key can be marked as requiring approval. A send from it is persisted and visible, but nothing goes out until a person releases it. This is the safety valve for giving an autonomous agent a sending credential — without it the choices are trusting the agent completely or not giving it email at all.
Marking a key
Set it when creating the key in the dashboard under API keys, or with requires_approval: true on POST /v1/api-keys. Creating keys over the API needs domains:write, the closest thing to an administrative scope: a key that can only send must not be able to mint one that can do more.
{
"name": "support-agent",
"scopes": ["emails:send", "emails:read", "threads:read"],
"requires_approval": true,
"daily_send_limit": 50
}What the agent sees
POST /v1/emails from such a key still returns 202, with status: "pending_approval" and an approval_id. The message exists in the log with status queued. Suppression, the recipient allowlist and the daily cap are all checked first, so a send that would have been refused is refused rather than queued for someone to reject.
That status is not an error and there is nothing to retry — sending again queues a second draft. Replaying the same Idempotency-Key returns the same held response.
{
"id": "…",
"status": "pending_approval",
"approval_id": "…",
"thread_id": "…",
"scheduled_at": null
}The queue
GET /v1/approvals (scope emails:read) lists what is waiting, oldest first, each with id, message_id, api_key_id, created_at, expires_at and the full message — to, subject, text, html — so a reviewer can read it in place. An approval expires 72 hours after it was requested and drops out of the list, so a forgotten draft never goes out days later, out of context.
Deciding
POST /v1/approvals/{id} (scope emails:send) takes a decision of approve or reject, a decided_by naming who authorised it, and an optional reason. Approving sends the message immediately and returns status: "approved" with the message_id. If the held send carried a scheduled_at still in the future, approving places it on the schedule instead and the response carries that scheduled_at; a hold cannot be walked around by scheduling. Rejecting marks the message canceled.
curl -X POST https://api.example.com/v1/approvals/$APPROVAL_ID \
-H "Authorization: Bearer $API_KEY" \
-d '{"decision": "approve", "decided_by": "ana@example.com"}'Once, not twice
The decision is a conditional update: it applies only while the approval is still pending and unexpired, and the second caller to arrive gets 409 already_decided. Two reviewers clicking at the same moment, or one double-clicking, cannot release the same message twice — the second attempt fails before anything is dispatched. If the release itself fails at delivery time the call answers 502, and the approval stays decided rather than reopening.