# Errors

> Common error envelope returned by the public API, the HTTP status to error-shape mapping, and how integrators should branch on errors.

Source: https://business-api-docs.youhodler.com/docs/concepts/errors

Every error response on the public API uses the same JSON envelope, so
an integration can write one parser that handles every failure mode.
HTTP status carries the broad category; the body carries a stable
machine-readable `code`, a human message, and an optional `details`
object with structured context.

> **Practical rule:** Branch your error handling on HTTP status first, then on `body.code`. Treat `details` as informational — it varies by error and is not part of the main control flow.

## Error Envelope

Every error response carries the same shape, regardless of the HTTP
status:

```json
{
  "code": "string",
  "message": "string",
  "details": { "...optional structured context..." }
}
```

- **`code`** — stable, machine-readable identifier for the error. Use
this in your branching logic.
- **`message`** — human-readable description, suitable for logs or
developer tooling. Not localized; not safe to surface to end users
verbatim.
- **`details`** — optional object with error-specific context (the
failing field, the conflicting resource id, the precondition that
did not match, and so on).

A machine-readable catalog of all currently-defined error shapes is
published at
[`/errors.json`](pathname:///errors.json) for tooling that wants to
consume it directly.

## HTTP Status Mapping

The HTTP status tells you which kind of failure happened. The same
status can carry different `code` values depending on the situation,
but the high-level meaning is fixed:

| Status | Meaning | Typical causes |
| --- | --- | --- |
| `400 Bad Request` | The request payload, query, or parameter shape is invalid. | Missing required field, malformed JSON, bad enum value. |
| `401 Unauthorized` | Authentication failed. | Missing, expired, or invalid bearer token. |
| `403 Forbidden` | Authenticated, but not allowed to perform this operation in this scope. | Capability not granted, scope outside granted authority. |
| `404 Not Found` | The referenced resource does not exist or is not visible to the caller. | Wrong id, resource in another tenant, resource retired. |
| `409 Conflict` | The request conflicts with the current state of the resource. | Idempotency-key mismatch, lifecycle precondition not met. |
| `412 Precondition Failed` | The `If-Match` ETag did not match the current resource version. | Concurrent update; re-read and retry. |
| `502 Bad Gateway` | An upstream system the platform depends on returned an error. | Transient infrastructure issue; safe to retry. |
| `503 Service Unavailable` | Operation temporarily not available. | Maintenance, load shedding; retry with backoff. |

For idempotency-related conflicts, see
[Idempotency And Concurrency](/docs/concepts/idempotency-and-concurrency).

## Code Conventions

`code` values are stable strings. The platform mixes two casings in the
current contract:

- short snake_case strings such as `unauthorized` or
`forbidden_capability_scope`
- screaming-snake-case strings such as `INVALID_REQUEST`

Both are stable identifiers — match on the exact string, not on
casing. New codes prefer snake_case going forward.

## How This Appears In The API

**Response — 401 application/json**
```json
{
  "code": "unauthorized",
  "message": "invalid credentials"
}
```
**Response — 400 application/json**
```json
{
  "code": "INVALID_REQUEST",
  "message": "missing required field",
  "details": {
    "reason": "missing_required_field",
    "field": "amount.value"
  }
}
```
**Response — 412 application/json**
```json
{
  "code": "precondition_failed",
  "message": "If-Match did not match current resource version",
  "details": {
    "current_etag": "v3:01HZ9X..."
  }
}
```
