Okouro docs

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 Requests carries 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 no x-ratelimit-* headers. See Rate limits.
  • 401 Unauthorized from the auth middleware (a missing or invalid bearer token) has no body at all - just the status code.

Status codes

CodeMeaningWhat to do
200Success with a bodyRead the body.
201CreatedRead the created resource.
202Accepted (async)The request was queued; no immediate result.
204Success, no bodyNothing to read (typical for deletes and logout).
400Bad requestThe body or query is malformed (validation failed). Fix the input.
401UnauthorizedMissing, invalid, or expired token. Refresh, then retry once.
403ForbiddenValid token, wrong role or no permission for this record. Do not retry.
404Not foundThe record does not exist or is outside your account. Do not retry.
409ConflictA uniqueness or state-machine rule was violated (duplicate email, illegal transition, quota reached).
410GoneA token (invitation, password change) has expired.
422Unprocessable entityThe input parses but breaks a business rule (negative money, day outside period, unknown category).
429Too many requestsRate limited. Back off per Retry-After.
500Server errorSomething failed server-side. Retry with backoff; if it persists, report it.
502Bad gatewayAn upstream payment provider failed. Retry later.

Validation: 400 vs 422

Both mean “your input was rejected”, and the line between them is fine:

  • 400 is 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).
  • 422 is 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 submitted or validated.
  • 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.