Errors.
Errors are always JSON. The HTTP status is the source of truth — handle it first, parse the body for human-readable detail second.
Envelope#
Every non-2xx response has at least this shape:
{
"error": "Human-readable description of what went wrong."
}
Validation errors add a structured errors map keyed by field name:
{
"error": "Validation failed",
"errors": {
"title": ["The title field is required"],
"priority": ["The priority must be one of: low, medium, high, urgent"]
}
}
Status codes you should handle#
| Status | Meaning | What to do |
|---|---|---|
| 200 | Success. Body is the resource (or { data, meta } for paginated lists). |
Carry on. |
| 201 | Resource created. Returned for POST on collections (issues, projects, comments, …). |
Body has the new resource, including its id. |
| 204 | Success, no body. Common on DELETE. |
Treat as OK. |
| 304 | Not Modified. The resource hasn't changed since the If-None-Match ETag you sent. |
Use your cached copy. |
| 400 | Bad request. Malformed JSON, missing required header, unparseable parameter. | Fix the request. Do not retry the same payload. |
| 401 | Unauthorized. Missing, expired, or revoked token. | Re-authenticate. Don't retry blindly — repeated 401s look like brute-force. |
| 403 | Forbidden. You're authenticated but not allowed: wrong role, PAT used on a cross-org endpoint, X-Org-Id doesn't match the key's bound org. | Check role and org binding. Read the message — it's specific. |
| 404 | Not found. Either the resource doesn't exist or your org can't see it. | Verify the ID and the X-Org-Id header. |
| 405 | Method not allowed. The path exists but not for that verb. | Check the OpenAPI spec for the allowed methods. |
| 409 | Conflict. Duplicate slug/key, concurrent edit, optimistic-lock failure. | Re-read the resource and reconcile. |
| 422 | Validation failed. Body shape passed the schema but values are wrong. | Inspect errors, fix the offending fields. |
| 429 | Too Many Requests. You blew the rate limit. | Honor X-RateLimit-Reset / Retry-After. See Rate limits. |
| 500 | Internal server error. Something blew up on our side. | Retry with backoff. If it persists, report it. |
| 502 / 503 | Upstream or gateway error. Transient. | Retry with exponential backoff (cap at ~30s). |
Retry policy#
- Idempotent methods (
GET,HEAD,PUT,DELETE) — safe to retry on5xxand429with exponential backoff. - Non-idempotent methods (
POST,PATCH) — retry only on429and on transport-level failures (connection reset, DNS, TLS handshake). Never retry a500onPOSTblindly — the resource may already exist. - 4xx (except
429) — never retry. The request is wrong, not the server. - Backoff — start at 500ms, double each attempt, cap at 30s, give up after 6 tries.
When the server returns 429, the X-RateLimit-Reset header tells you how many seconds until your window refills. Sleeping that long beats hammering the endpoint with linear retries.
Common errors and what they mean#
401 Unauthorized · "Authentication required"
You sent no token, or the token is invalid. Confirm the Authorization header is present and starts with Bearer pjk_live_.
403 Forbidden · "API key bound to a different organization"
You sent X-Org-Id pointing at an org the key is not bound to. Either drop the header (the key already knows its org) or send the right UUID.
403 Forbidden · "Not available for API key auth — use a user session"
The endpoint is intentionally off-limits to PATs (creating orgs, joining via invite code, cross-org search). Sign in to the web app for those operations.
422 Validation failed
Look at errors. Each key is the field name that broke; the value is an array of human-readable messages. Fix them, retry.
429 Too Many Requests · "Rate limit exceeded. Try again later."
You're over your per-token budget. See Rate limits for the exact numbers and headers.