Error Handling
Know Exactly What Went Wrong
One error envelope, clear retry semantics, and a code for every failure. When Archie cannot finish a call, he tells you precisely why and what to do next. Branch on the code, never on the message.
The Error Envelope
Every error returns the same JSON shape. The type groups the failure; the code is the stable, machine-readable discriminator you branch on; the message is human-readable and may change, so never parse it.
{
"error": {
"type": "validation_error",
"code": "missing_field",
"message": "workspace_id is required",
"field": "workspace_id",
"correlation_id": "req_01HX...",
"retryable": false
}
}The correlation_id traces the full call in your dashboard. Quote it when you contact support, and log it so any failure is reproducible. retryable tells you whether trying again can help.
HTTP Status Codes
| Status | Meaning | What to do |
|---|---|---|
400 | Bad request. Malformed body. | Fix the request shape. Not retryable. |
401 | Unauthorized. Missing or invalid credentials. | Re-authenticate. See Authentication. |
403 | Forbidden. Authenticated but out of scope. | Request a broader scope, or use a key that has it. |
404 | Not found, or you cannot see it. | Check the id and your access. Not retryable. |
409 | Conflict. A concurrent change or duplicate. | Reconcile state, then retry once the conflict clears. |
422 | Unprocessable. Understood but failed validation. | Read field and fix the value. |
429 | Rate limited. | Honour Retry-After. See Rate limits. |
5xx | Server error on our side. | Retry with exponential backoff. |
Retry Semantics
Retry only what is safe to retry. Reads are idempotent and always safe. For writes through the skills API, send an Idempotency-Key header. A retried call carrying the same key within 60 seconds replays the original response instead of running again, so a network retry cannot post twice. A response built from a replay carries X-Idempotent-Replayed: true.
- Retry on
429and5xx. Back off exponentially with jitter, capped at a handful of attempts. - Do not retry on
400,401,403,404, or422. The same request fails the same way. Fix it first. - On
429, readRetry-After(seconds) and wait at least that long. The rate-limit headers tell you when the window resets.
import time, random
def call_with_retry(fn, attempts=4):
for i in range(attempts):
resp = fn()
if resp.status_code < 400:
return resp
if resp.status_code == 429:
wait = float(resp.headers.get("Retry-After", 2 ** i))
time.sleep(wait)
continue
if resp.status_code >= 500:
time.sleep((2 ** i) + random.random())
continue
# 4xx other than 429: fix the request, don't retry
resp.raise_for_status()
resp.raise_for_status()Code Dictionary
| Code | Type | Meaning |
|---|---|---|
auth_required | authentication | No credential presented. |
invalid_token | authentication | The token is malformed, expired, or revoked. |
insufficient_scope | authorization | Authenticated, but the scope does not cover this action. |
workspace_not_found | authorization | No workspace context, or no access to it. |
missing_field | validation | A required field is absent. See field. |
invalid_field | validation | A field failed validation. See field. |
not_found | not_found | The resource does not exist or is not visible to you. |
conflict | conflict | A concurrent change or a duplicate write. |
rate_limited | rate_limit | Too many requests. Honour Retry-After. |
key_credit_exhausted | rate_limit | A test key spent its credit cap. |
internal_error | server | An unexpected failure. Retry with backoff; quote the correlation id. |
code once and your handling works across surfaces.