Errors
Every error uses one JSON envelope, with a machine-readable code where it helps.
All errors return the same shape:
{
"error": "Human-readable message",
"code": "OPTIONAL_MACHINE_CODE",
"details": { "optional": "structured context" }
}error is always present. code and details appear when there is something structured to say.
Status codes
| Status | Meaning |
|---|---|
400 | The request body, query, or params failed validation. |
401 | Missing or invalid credentials. |
403 | Authenticated, but not allowed — no active organization, or your role lacks the required permission. |
404 | The resource doesn't exist, is archived, or belongs to another organization. |
409 | Conflict with current state — for example archiving a stage that still has active journeys. |
429 | Rate limit hit — see Rate limits. The Retry-After header says how long to wait. |
500 | Something broke on our side. The body is { "error": "Internal server error" } with no details. |
Validation errors
Failed validation returns 400 with code: "VALIDATION_ERROR" and the individual issues in details:
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{
"path": ["email"],
"code": "invalid_format",
"message": "Invalid email address"
}
]
}Each issue has a path into the offending field, a machine code, and a message. Fix the listed fields and retry — validation errors are never transient.
404 instead of 403 on a specific resource is intentional: the API does not reveal whether an ID exists in another organization.