Errors come back with a standard HTTP status code and, almost always, a small JSON body explaining what went wrong.
The error body
Failures return a single-field JSON object:
{ "error": "client not found" }
There is no code, message, or details field. The human-readable error string is all you get, so branch on the HTTP status code, not on the text.
Two exceptions to this shape:
429 Too Many Requestscarries an extra field:{ "error": "rate_limited", "message": "Too many requests; please slow down." }. On the credential endpoints (login, signup, refresh, invitation and password-change redemption) the body is instead the generic{ "error": "too many attempts; please try again later" }, with nox-ratelimit-*headers. See Rate limits.401 Unauthorizedfrom the auth middleware (a missing or invalid bearer token) has no body at all - just the status code.
Status codes
| Code | Meaning | What to do |
|---|---|---|
200 | Success with a body | Read the body. |
201 | Created | Read the created resource. |
202 | Accepted (async) | The request was queued; no immediate result. |
204 | Success, no body | Nothing to read (typical for deletes and logout). |
400 | Bad request | The body or query is malformed (validation failed). Fix the input. |
401 | Unauthorized | Missing, invalid, or expired token. Refresh, then retry once. |
403 | Forbidden | Valid token, wrong role or no permission for this record. Do not retry. |
404 | Not found | The record does not exist or is outside your account. Do not retry. |
409 | Conflict | A uniqueness or state-machine rule was violated (duplicate email, illegal transition, quota reached). |
410 | Gone | A token (invitation, password change) has expired. |
422 | Unprocessable entity | The input parses but breaks a business rule (negative money, day outside period, unknown category). |
429 | Too many requests | Rate limited. Back off per Retry-After. |
500 | Server error | Something failed server-side. Retry with backoff; if it persists, report it. |
502 | Bad gateway | An upstream payment provider failed. Retry later. |
Validation: 400 vs 422
Both mean “your input was rejected”, and the line between them is fine:
400is returned when the payload cannot be parsed or fails basic field validation (a missing required field, an unparseable enum, a password that is too short).422is returned when the payload parses cleanly but violates a domain rule (a negative amount, a day outside the timesheet’s period, an assignment that does not match).
Treat both as “fix the request before sending it again”.
State-machine conflicts: 409
Several resources are state machines, and acting out of order yields 409. Examples:
- Editing or deleting a timesheet that is no longer
draft. - Submitting a timesheet that is already
submittedorvalidated. - Cancelling an expense that is already
paid. - Creating a client when the plan quota is reached.
- Registering with an email that already exists.
When you get 409, re-read the resource to see its current state rather than blindly retrying.
A handling pattern
send request
-> 2xx: done
-> 401: refresh once, retry once; on second 401, log out
-> 400 / 422: surface the error text, do not retry
-> 403 / 404: do not retry (permission or existence)
-> 409: re-read the resource, reconcile, then decide
-> 429: wait for Retry-After, then retry
-> 5xx: retry with exponential backoff, cap attempts
Never retry 403 or 404; they will not change without a different request or different credentials.