Skip to main content

Errors and HTTP status codes

The Bezala API uses standard HTTP status codes to signal what happened. The body of a non-2xx response is a JSON object with at least an error field describing what went wrong.

J
Written by Julia Winberg

Status codes you'll actually see

Code

Meaning

Typical cause

200 OK

The request succeeded.

Normal GET.

201 Created

A new resource was created.

Successful POST.

204 No Content

The request succeeded; nothing to return.

Successful DELETE.

400 Bad Request

The request body or query string was malformed.

Wrong content type, invalid JSON, unknown filter values.

401 Unauthorized

The token was missing, wrong, or revoked.

No Authorization header, or expired token.

403 Forbidden

The token is valid but the user can't do this.

A regular user trying to read another user's expenses.

404 Not Found

The path doesn't exist, or the record doesn't exist for this user.

Wrong endpoint URL, or an ID the user can't see.

422 Unprocessable Entity

The request was well-formed but failed validation.

Required field missing, value out of range, state transition forbidden.

429 Too Many Requests

You're calling too fast.

Sustained high call volume.

500 Internal Server Error

Something broke on our side.

Worth retrying once; if it persists, contact support.

The most common ones you'll handle in code are 401, 404, and 422.

Error response shape

Every error response is JSON. The minimum is:

{ "error": "Invalid email or password." }

Validation errors on POST and PUT typically include field-level detail:

Some endpoints additionally include a top-level message for display in a UI. The shape isn't perfectly uniform across the whole API yet — defensive parsing is your friend.

Distinguishing 403 from 404

Bezala generally prefers 404 over 403 to avoid leaking information. If you ask for a record that exists in the database but doesn't belong to the company you're authenticated against, you'll typically get 404, not 403. From your client's perspective they're the same: the record is unreachable.

You'll see real 403s when you try to perform an action that the user could ask about but isn't allowed to take — for example, a regular user calling PUT /api/transactions/:id/approve on someone else's receipt.

Retrying on transient failures

The API is generally reliable. When transient failures do happen, they're worth retrying:

  • 429 Too Many Requests. Back off and try again. Exponential backoff with jitter (start at 1 second, double each retry, cap at 60) works well.

  • 500, 502, 503, 504. Same approach. After three retries spaced apart, give up and surface the error.

What you should not do:

  • Don't retry 400, 401, 403, 404, or 422 automatically. These are deterministic — retrying without changing the request will produce the same error.

  • Don't retry a POST without checking what happened. If you got a network error and you don't know whether the create went through, look up the record by external_id (if you set one) or by some natural key before re-sending.

Validation errors on state transitions

Many of Bezala's "errors" aren't really errors in the bug sense — they're refusals based on the lifecycle state of the record. Examples:

  • Trying to update a receipt that's already in the accounted state. The accounting batch has been generated; the record is locked.

  • Trying to delete an absence whose reported_at field is set. It's been reported to payroll.

  • Trying to create a budget that overlaps with an existing budget for the same user and expense account.

These come back as 422. The body explains why. Don't treat them as bugs to retry through — they're intentional invariants. See the expense lifecycle for which transitions are allowed when.

Logging and debugging

When you're integrating, log every non-2xx response in full. Specifically log:

  • The HTTP status.

  • The full response body.

  • The request method and URL.

  • The request body (with sensitive fields like passwords masked).

  • A correlation ID of your own — a UUID generated for the call — that you also write to your local logs.

That's enough to reproduce 95% of issues without going back and forth with support.

If you need to ask us for help, include the correlation ID, the timestamp (with timezone), the user's email, and the response body. We can match that against our server logs quickly.

Did this answer your question?