Error shape
The body is flat — nothing nests under an error key:
JSON
{
"code": "validation_failed",
"message": "The request does not match the contract",
"details": {
"issues": [
{
"path": ["content", "subject"],
"code": "too_small"
}
]
},
"requestId": "01a0ba31-3fe4-7cc4-a1c6-64bdaaa5d99c"
}Response attributes
codestringRequiredFrom the closed set in the table below.
messagestringRequiredReadable, but not meant for an end user — we never translate it.
detailsfree-form map | nullRequirednull unless the code has something structured to add, like the list of fields that failed validation.
requestIdstringRequiredThe same id that arrives in the x-request-id header of every response — error or not — so you can capture it even when your client never reads the body.
Codes this surface can answer with
Of the codes the system carries, these are the ones a /v1 caller can actually receive:
| Code | HTTP status | When it shows up |
|---|---|---|
bad_request |
400 | A rule the schema alone cannot express — POST /v1/email with no address across to, cc and bcc. |
unauthorized |
401 | The bearer token is missing, or does not match any active key. |
api_key_expired |
401 | The bearer token names a key that is past its own expiration. The key stays revocable and visible in the panel; it just no longer authenticates. |
payment_required |
402 | The organization is over its plan’s sending limit. |
forbidden |
403 | The key is valid, but does not carry the permission this action needs. |
not_found |
404 | The resource does not exist, or does not exist for this organization — the response never reveals which. |
conflict |
409 | The idempotencyKey was already used with a different body, or the referenced template is not active. |
validation_failed |
422 | The body does not match the schema — an unrecognized field, a wrong type, more than 50 addresses in one field. |
rate_limited |
429 | This key exceeded its per-second request limit. |
internal |
500 | Something failed on our side. |
The full set is longer — the rest are codes for flows /v1 does not expose, like an expired
invitation — but any client can safely treat a code it does not recognize as a generic error.