The error format, every error code the API returns, and what to do about each one.
Every error has the same shape:
{ "code": "INSUFFICIENT_SCOPE", "error": "This API key is missing the `write` scope. Edit the key in your workspace settings to add it.", "details": { "required": "write", "granted": ["read"] }}
Field
Description
code
Machine-readable code, UPPER_SNAKE_CASE. A stable contract: branch your logic on it.
error
Human-readable message. It may be reworded: never parse it.
details
Optional. Context that helps fix the request: faulty fields, the bound that was exceeded, ids.
A business refusal returns a 4xx, not a 500: it describes a request you can fix, and retrying it unchanged will never succeed.
details.required names it, details.granted lists what the key has.
OUTBOUND_SEND_NOT_ALLOWED
The action makes email leave the workspace and the key does not have the Send emails permission.
Create a key with Send emails ticked.
WORKSPACE_SUSPENDED
The workspace is suspended.
Reactivate the subscription.
OUTBOUND_SEND_NOT_ALLOWED is not a missing scope but a separate permission: write alone must not be enough to write to your customers, or to forward their mail elsewhere, if the key leaks. It is returned by POST /drafts/send, by every write on /auto-forward-rules, and by /ai-label-rules when a request sets autoReplyEnabled: true.
GET /emails/{emailId}, and label actions on an email.
NOT_FOUND
GET /emails/{emailId}/body, PATCH /emails/{emailId}, DELETE /emails/{emailId}.
DRAFT_NOT_FOUND
GET /drafts/{draftId}.
MAILBOX_NOT_FOUND
POST /drafts, POST /drafts/send and POST /ai-label-rules/backfill, when mailboxId is unknown.
LABEL_NOT_FOUND
The /labels/{labelId} endpoints, applying a label to an email, and POST /emails/bulk with a label action.
RULE_NOT_FOUND
The /ai-label-rules/{ruleId} and /auto-forward-rules/{ruleId} endpoints.
VIEW_NOT_FOUND
The /views/{viewId} endpoints.
FILTER_NOT_FOUND
The /filters/{filterId} endpoints.
A resource that belongs to another workspace returns 404, not 403: a 403 would confirm that it exists. Treat EMAIL_NOT_FOUND and NOT_FOUND alike: both mean the email is not reachable with this key.
Idempotency-Key reused with a different body, or the original call is still in progress.
DUPLICATE_TAG_NAME
A label with this name already exists in the workspace.
DUPLICATE_RULE
An AI label with this name already exists in the workspace.
DUPLICATE_FILTER
An inbox filter with this type and value already exists (details.filterId names it).
For CONFLICT, generate a new key if you are describing a new operation, or retry in a moment if the original call is still running. Label names are unique per workspace, manual and AI labels alike.
Unexpected failure, including a refusal from the email provider on POST /drafts/send.
This is the only case where retrying the same request makes sense. On a send, nothing was sent and the idempotency key is released, so a retry with the same key sends again rather than replaying a failure. If a 500 persists on a request you believe is valid, report it with the time and the path you called.