Errors

Failures are one flat object with a stable machine code and a message written for a human. The HTTP status carries the category, the code carries the specific reason, and the message says what to do about it.

The envelope

Success responses wrap their payload in data; collections add pagination:

Success
{ "data": { … } }

{ "data": [ … ],
  "pagination": { "limit": 50, "offset": 0, "total": 1234, "has_more": true } }

Every failure is the same shape:

Error
{ "error": { "code": "unauthorized", "message": "Invalid or missing API key." } }

Branch on error.code — codes are stable machine identifiers. Messages are for people and logs; their wording can improve over time.

Status codes

StatusCodeMeaning and what to do
400bad_requestThe request itself is malformed — for example, a body that is not valid JSON. Fix the request.
400invalid_inputThe body parsed but failed the action’s schema. The message lists what to fix — see the format below.
401unauthorizedMissing, malformed, unknown or revoked credential. The response carries WWW-Authenticate: Bearer. Check the token, then verify with GET /api/v1.
402plan_feature_requiredThe action belongs to a plan feature the workspace doesn’t currently carry. The message names it; upgrading in the app turns the action on.
403forbiddenThe credential is valid but its scope (read/write) or role doesn’t cover this operation. A token never exceeds the permissions of the member behind it.
404not_foundNo such record — or no such action: an action your credential may not run is reported as missing, so the catalog you can list and the actions you can call are the same set.
409duplicateThe write conflicts with a record that already exists (for example, a contact with the same phone number). Read the existing record instead of retrying.
422insert_failed and friendsThe input was valid but the operation was refused — the message carries the store’s reason. Not retryable as-is.
429rate_limitedBudget spent for this window. Honor Retry-After — details on the rate limits page.
500server_errorSomething failed on Chirply’s side. Safe to retry with backoff.

Validation detail — invalid_input

When an action’s arguments fail its schema, the message is the flattened issue list: each entry is path: message, joined with semicolons, up to the first six issues. A problem with the body’s root reports its path as (root).

Example
{
  "error": {
    "code": "invalid_input",
    "message": "phone: Invalid phone number; tags.0: Expected string, received number"
  }
}

The authoritative field list for any action is its JSON Schema — GET /api/v1/actions/<name> or the actions reference. Validate against it client-side and invalid_input becomes a bug report, not a runtime branch.

The OAuth exception

One endpoint deliberately speaks a different dialect: POST /api/v1/oauth/token answers errors in the RFC 6749 §5.2 shape, the flat object every OAuth library already parses:

{ "error": "invalid_grant", "error_description": "This refresh token is no longer valid. Reconnect." }

Its codes are the registered ones — invalid_request, invalid_client, invalid_grant, unsupported_grant_type — and when it throttles you it answers 429 with slow_down and the same Retry-After header as everywhere else.

Retry guidance

  • 429 — wait the seconds in Retry-After, then retry. The window is short (one minute), so a queued retry nearly always succeeds.
  • 500 and network failures — retry with exponential backoff and a small cap. Make writes idempotent on your side where you can: check for the record before recreating it.
  • Every other 4xx — the request will fail the same way again until something changes. Fix the input (400), the credential (401/403), or the plan (402) first.

A healthy client fails once

Alert on repeated 401s instead of looping on them — a revoked or rotated credential answers the same way every time, and unidentified traffic spends the shared per-address budget described under rate limits.