Errors
Uniform response shape. Status code conveys category; body carries the message and optional validation detail.
Response shape
Every error response is a JSON body with the following shape:
{
"error": "human-readable message",
"details": { ... } // optional — present on validation failures
}Success responses use {"data": ...} or a resource-specific shape. If you see an error key, something went wrong.
Status codes
| Code | Meaning | Action |
|---|---|---|
| 200 | OK — successful read or update | None |
| 201 | Created — successful POST | Capture the id from the response body |
| 400 | Validation failed — body or query violates schema | Check details for per-field errors |
| 401 | Unauthorized — missing, malformed, or revoked key | Verify key, check capitalization, not retriable |
| 403 | Forbidden — key lacks required scope | Generate a write-scoped key for mutations |
| 404 | Not found — resource doesn't exist OR isn't in your org | Verify id; we deliberately don't distinguish |
| 409 | Conflict — unique constraint violated | Check for duplicate name/label/phone, not retriable |
| 429 | Rate limited | Wait Retry-After seconds, then retry. Details |
| 500 | Internal error | Retry with backoff; if persistent, contact support |
404 note: requesting a resource that exists in a different org returns 404 (same as a truly-nonexistent id). This is deliberate — returning 403 would leak the existence of cross-org resources.
Validation errors (400)
When a request body or query violates the schema, the details field contains a flattened Zod error. Example:
POST /v1/contacts
{ "phone_number": "555" }
HTTP/1.1 400 Bad Request
{
"error": "Validation failed",
"details": {
"formErrors": [],
"fieldErrors": {
"phone_number": ["String must contain at least 10 character(s)"]
}
}
}formErrors holds top-level issues (e.g. the body couldn't be parsed as an object at all). fieldErrors maps each failing field to its error messages.
Specific error codes
Beyond HTTP status, some endpoints return semantic error strings that are stable enough to branch on:
| error string | Context |
|---|---|
| unauthorized | Any 401 response |
| insufficient_scope | 403 when write endpoint called with read-only key. required field indicates which scope is needed. |
| rate_limited | Any 429 response |
| Validation failed | 400 on body/query validation. Check details. |
Freeform error messages (e.g. "A contact with this phone number already exists") are human-readable but not guaranteed to stay stable across versions. Branch on status code first, semantic error string second, message text never.
Retrying
- Safe to retry: 429 (respect
Retry-After), 500, network timeouts, connection resets. - Not safe to retry without investigation: 400, 401, 403, 404, 409. These indicate something specific about the request; blindly retrying won't fix them.
All non-idempotent operations (POST, some PUTs) are non-idempotent by design — we don't deduplicate based on request fingerprint. If you need safe retries on writes, check with a GET before POSTing again.