Errors
Use the HTTP status to decide how to handle a failure, then error.code to identify the cause. Correct invalid requests, wait for rate limits, and reconcile uncertain writes before starting another operation. Standard errors use this JSON envelope:
{
"error": {
"type": "invalid_request_error",
"code": "schema_not_found",
"message": "Schema not found."
}
}
A success body is { "data": ... }, plus a pagination block on list endpoints and an optional top-level warnings array of strings. An aborting event batch is an exception: HTTP 422 returns data.results, data.summary, and data.failedIndex, so the caller can see rollback results. Inspect that business-result envelope before applying the standard error parser.
The error object
- Name
type- Type
- string
- Description
The coarse category, derived from the status class.
invalid_request_errorfor every 4xx,api_errorfor every 5xx. Clients should tolerate a missingtypeon an unresolved-operation response.
- Name
code- Type
- string | null
- Description
The stable, machine-readable cause. Branch on this. It is
nullwhen the failure has no specific code (most400and403cases, and most5xx). Some server failures carry a code, includingidempotency_outcome_unknown.
- Name
message- Type
- string
- Description
A human-readable description. For 4xx it explains the failure. For 5xx it is a generic per-status label and never leaks internal detail.
- Name
issues- Type
- string[]
- Description
Present on validation failures that expose details: for example several readiness problems for
broadcast_invalid, or the missing attribute key forrequired_attribute_missing.
Status codes
Common response statuses are listed below. Successful calls include 200, 201 when a record, segment, or webhook is created, and 202 on events.fire and broadcasts.send, so do not test for 200 alone.
| Status | type | Typical cause |
|---|---|---|
400 Bad Request | invalid_request_error | Input failed validation, malformed_json, or offset_too_large |
401 Unauthorized | invalid_request_error | Missing or unknown API key |
402 Payment Required | invalid_request_error | insufficient_credits or email_limit_reached |
403 Forbidden | invalid_request_error | Valid key, but the membership lacks the needed permission |
404 Not Found | invalid_request_error | Schema, record, or route not found |
405 Method Not Allowed | invalid_request_error | events_immutable: events cannot be edited or deleted |
409 Conflict | invalid_request_error | Identity collision, duplicate, or in-use resource |
413 Payload Too Large | invalid_request_error | Request body over 10 MB or normalized event input over 256 KiB |
415 Unsupported Media Type | invalid_request_error | Unsupported request charset or content encoding |
422 Unprocessable Entity | invalid_request_error | A validation gate or AI generation step failed |
429 Too Many Requests | invalid_request_error | Rate-limit bucket exceeded |
500 Internal Server Error | api_error | Unexpected server-side failure |
501 Not Implemented | api_error | Endpoint not yet implemented |
502 Bad Gateway | api_error | Upstream failure |
503 Service Unavailable | api_error or omitted | A service is unavailable or an operation outcome could not be persisted |
Common codes
code is the value to branch on. This is a representative set; individual endpoints document their own codes. Any failure without a specific code returns code: null, so fall back to the status.
| Code | Status | Emitted when |
|---|---|---|
invalid_api_key | 401 | The API key is missing or unknown |
insufficient_credits | 402 | An AI-backed call, or a write into a schema with AI auto-fill, has no credits |
email_limit_reached | 402 | A templates.send exceeds the email allowance |
endpoint_not_found | 404 | The path matched no route |
schema_not_found | 404 | The :schema segment resolved to no schema, or the wrong schema type for the endpoint |
record_not_found | 404 | A contact or object id matched no record in the schema |
events_immutable | 405 | You attempted to update or delete an event |
identifier_conflict | 409 | A create or update collides with another record's identity |
suppression_already_exists | 409 | An identical suppression already exists in your space |
malformed_json | 400 | The request body is not valid JSON |
payload_too_large | 413 | The body exceeds 10 MB |
rate_limit_exceeded | 429 | A request rate bucket was exceeded |
required_attribute_missing | 400 | A required writable field has no value or default; issues names the key |
event_id_conflict | 409 | The same event occurrence identifier received different normalized input |
event_erased | 409 | The occurrence was erased and cannot be replayed |
event_acceptance_unknown | 409 | An earlier occurrence has no recoverable acceptance receipt |
event_rate_limited | 429 | Event admission rate exceeded, separate from API request buckets |
event_backlog_full | 429 | Too many pending event preparations |
event_receipt_limit | 429 | Receipt capacity is full; waiting alone may not free it |
idempotency_conflict | 409 | The same operation is still processing |
idempotency_outcome_unknown | 409 or 503 | The original operation needs reconciliation |
idempotency_key_reused | 422 | The same key and path received a different body |
idempotency_admission_busy | 429 | Operation admission is busy; retry the same key after the indicated delay |
identifier_conflict reflects the contact identity cascade: userId, then email, then phone for contacts, and externalId for objects. Use records/upsert to create-or-update by identity instead of failing on a strict create.
Server errors
A 5xx means the request could not complete normally. Standard error responses use type: "api_error" and a generic message. Keep the status, code, operation key, and any returned operation identifier in your logs.
{
"error": {
"type": "api_error",
"code": null,
"message": "Internal server error."
}
}
A read can usually be retried with bounded exponential backoff and jitter. For a write, the response alone may not establish whether the action took effect. On supported endpoints, retain the original Idempotency-Key and payload. A recorded error can be replayed, and an unresolved outcome requires investigation.
If the server reports idempotency_outcome_unknown, inspect the affected
resource or run history before starting a new operation. A 503 produced
while persisting an outcome can contain error.operationId and omit
error.type; branch on the status and code without assuming type is
present.
Rate limits
A 429 means admission was refused. Rate and backlog limits can recover after a delay; receipt or identity capacity exhaustion can require operational reconciliation. Rate-limit failures use rate_limit_exceeded; an idempotent operation that cannot yet be admitted can use idempotency_admission_busy. Respect Retry-After before repeating the request.
The header can be an HTTP date or a delay in seconds. Handle both, and use a fallback if it is absent or invalid:
function retryAfterMs(value, now = Date.now()) {
if (!value) return 1000
const seconds = Number(value)
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
const date = Date.parse(value)
return Number.isFinite(date) ? Math.max(0, date - now) : 1000
}
const waitMs = retryAfterMs(response.headers.get('Retry-After'))
const delayWithJitter = waitMs + Math.random() * 250
Use that delay in a bounded retry loop. See rate limits for request budgets and batching.
Handling errors
Classify the outcome before retrying. These decisions apply whether you call the API directly or through a client library:
| Condition | Next action |
|---|---|
401 | Check the configured key; do not keep retrying an invalid credential. |
403 | Check the key's space and the owner's permissions. |
| Validation, identity, or consent error | Correct the cause. Treat a corrected body as a separate operation if the original key has been claimed. |
429 | Respect Retry-After; preserve the original key and body. Check capacity errors before scheduling more retries. |
idempotency_conflict | Wait, then retry the same operation. |
idempotency_outcome_unknown | Reconcile the original action before creating another one. |
5xx or a timeout on a read | Retry with a bounded backoff. |
5xx or a timeout on a write | Follow the endpoint's idempotency or deduplication contract; do not blindly resend. |
A successful HTTP status can still contain an operation-level failure. For example, templates.send can return 200 with data.status: "failed". Inspect the documented result fields as well as response.ok. Template failed also covers uncertain provider outcomes and retryable refusals; retain emailId and the operation key and reconcile before issuing a new send.