Every error returns the same envelope, so a client parses one shape:
{
"name": "UnauthorizedError",
"code": "missing_api_key",
"message": "Missing API key.",
"fix": "Send `Authorization: Bearer <key>`.",
"docs": "https://docs.carlyemail.com/authentication"
}
name stringrequired
The error class. Stable — safe to branch on.
message stringrequired
Human-readable. Not stable; do not parse it.
code string
Machine-readable reason. This is what to branch on.
fix string
What to actually do about it, when there is a specific answer.
docs string
A link to the page that explains it.
Status codes
| Status | Meaning |
|---|---|
400 |
The request is malformed or a field is invalid |
401 |
No key, or the key is invalid or revoked |
403 |
Authenticated, but not allowed — wrong scope, missing permission, unverified organization, or suspended |
404 |
No such resource, or not one this key can see |
409 |
Conflict — a username already taken, for instance |
429 |
Quota exceeded. Carries Retry-After |
Codes worth handling
| Code | Status | What it means |
|---|---|---|
missing_api_key |
401 | No Authorization header |
invalid_api_key |
401 | Unknown or revoked key |
invalid_token |
401 | An OAuth access token failed validation |
organization_unverified |
403 | Confirm the owner email before sending |
organization_suspended |
403 | Contact support |
insufficient_permissions |
403 | The key's permission map does not grant this |
inbox_out_of_scope |
403 | An inbox-scoped key reached for a different inbox |
quota_exceeded |
429 | See limits |
inbox_limit_reached |
403 | Plan inbox cap |
username_unavailable |
409 | Usernames are globally unique per domain |
suppressed_recipient |
403 | The address bounced or complained — see suppression |
A note on 403 versus 404
A resource belonging to another organization returns 404, not 403. Confirming
that something exists but is not yours is itself a disclosure, so the answer is
the same as if it did not exist.