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

CodeMeaningAction
200OK — successful read or updateNone
201Created — successful POSTCapture the id from the response body
400Validation failed — body or query violates schemaCheck details for per-field errors
401Unauthorized — missing, malformed, or revoked keyVerify key, check capitalization, not retriable
403Forbidden — key lacks required scopeGenerate a write-scoped key for mutations
404Not found — resource doesn't exist OR isn't in your orgVerify id; we deliberately don't distinguish
409Conflict — unique constraint violatedCheck for duplicate name/label/phone, not retriable
429Rate limitedWait Retry-After seconds, then retry. Details
500Internal errorRetry 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 stringContext
unauthorizedAny 401 response
insufficient_scope403 when write endpoint called with read-only key. required field indicates which scope is needed.
rate_limitedAny 429 response
Validation failed400 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

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.