Errors
The API uses conventional HTTP status codes and returns a consistent JSON error envelope you can branch on programmatically.
Error envelope#
Every error response carries a detail object with a stable code, a human-readable message, and optional details:
{
"detail": {
"code": "INSUFFICIENT_SCOPE",
"message": "This endpoint requires a read_write token.",
"details": { "token_scope": "read", "required": "read_write" }
}
}Status codes#
| Status | Meaning |
|---|---|
| 401 | Missing or invalid token. |
| 402 | Insufficient credits for a charged operation. |
| 403 | Authenticated, but the token's scope is insufficient. |
| 404 | The requested resource doesn't exist. |
| 422 | Request body or parameters failed validation. |
| 429 | Rate limit exceeded — back off and retry. |
Error codes#
The code values you may encounter:
| Code | Status | When |
|---|---|---|
MISSING_AUTH_HEADER | 401 | No Authorization header sent. |
INVALID_TOKEN_FORMAT | 401 | Header present but not a cornect_ bearer token. |
INVALID_TOKEN | 401 | Token not found or revoked. |
TOKEN_EXPIRED | 401 | Token is past its expiry date. |
TOKEN_NOT_WORKSPACE_BOUND | 401 | Legacy token with no workspace — mint a new one. |
NOT_WORKSPACE_MEMBER | 401 | Token owner was removed from the workspace. |
INSUFFICIENT_SCOPE | 403 | A read token called a read_write endpoint. |
INVALID_CURSOR | 422 | A pagination cursor that was edited, hand-built, or is no longer valid. Restart from the first page. |
CURSOR_SORT_MISMATCH | 422 | A cursor reused under a different sort than the one that produced it. Restart, or re-send the original sort_by/sort_order. |
RATE_LIMITED | 429 | Too many requests in the window. Honour Retry-After; see Rate Limits. |
no_candidates | 400 | An export matched no companies — there is nothing to export. |
insufficient_balance | 402 | Your balance cannot cover the export. details carries shortfall, cost, balance and matched_count. Nothing is charged. |
Exports are all or nothing. A balance that cannot cover the cost — zero, or merely short — returns this error and applies nothing: no credits charged, no job created, no companies unlocked. There is no partial export and no truncated file.
Read detail.details.shortfall to tell the user how many more credits they need; cost, balance and matched_count are there too, so no second call is needed. To avoid the round trip, call Preview Export first — it is free, and its would_skip is the same shortfall.
Why some codes are UPPERCASE and some are lowercase#
The casing is a convention, not an inconsistency, and it tells you what kind of failure you are looking at:
| Casing | Meaning | Examples |
|---|---|---|
UPPER_SNAKE | A protocol or validation failure — the request itself is wrong and will keep failing until you change it. | MISSING_AUTH_HEADER, INVALID_TOKEN, INSUFFICIENT_SCOPE, INVALID_CURSOR |
lower_snake | A business outcome — the request was perfectly well-formed, and this is the answer. | no_candidates, insufficient_balance |
A lowercase code is not an error in your integration: a search that matches nothing, or an account that has run out of credits, is a legitimate state your code should handle rather than a bug to fix. Match on the exact string either way — the codes are stable, and we will not re-case an existing one, because that would break every client already branching on it.
Handling errors in code#
Branch on detail.code rather than parsing messages. A robust handler distinguishes auth failures (fix the token), scope/credit failures (fix the request or top up), and 429 (retry with backoff):
429: its body carries the envelope like every other error, with detail.code = "RATE_LIMITED". For a transition period it also carries a top-level error string duplicating the message — that key is deprecated and will be removed; read detail.async function call(url, init) {
const res = await fetch(url, init);
if (res.ok) return res.json();
if (res.status === 429) throw new Error("Rate limited — retry with backoff");
const { detail = {} } = await res.json().catch(() => ({}));
switch (detail.code) {
case "INVALID_TOKEN":
case "TOKEN_EXPIRED":
throw new Error("Auth failed — check your token");
case "INSUFFICIENT_SCOPE":
throw new Error("Use a read_write token");
case "insufficient_balance":
throw new Error("Out of credits — top up at /account/credits");
default:
throw new Error(detail.message || `HTTP ${res.status}`);
}
}For deeper retry/idempotency guidance see the Error Handling guide.