# Errors

Every error has the same shape and a stable code your code can branch on.

When a request fails, the status code says what kind of failure it is and the body says exactly which:

```json title="404 Not Found"
{
  "statusCode": 404,
  "code": "lead.notFound",
  "message": "Lead 0192f0c1-7a3e-7c51-9d2e-4b7f0a1c2d3e not found",
  "params": { "id": "0192f0c1-7a3e-7c51-9d2e-4b7f0a1c2d3e" }
}
```

- **`code`** is the contract. It never changes meaning, so branch on it.
- **`message`** is English for you, the developer. Don't show it to your users or parse it.
- **`params`** holds the values the code refers to, such as the id that wasn't found.

## Validation errors

A request whose body or query doesn't validate gets **400** `request.invalid`, with one entry per failing field:

```json title="400 Bad Request"
{
  "statusCode": 400,
  "code": "request.invalid",
  "message": "Request validation failed",
  "params": {},
  "issues": [{ "field": "companyName", "constraints": ["isLength"] }]
}
```

## Codes you will meet

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `request.invalid` | Fix the fields listed in `issues`. |
| 401 | `auth.unauthenticated` | Check the credential: missing, mistyped, revoked or expired. |
| 403 | `auth.forbidden` | The credential lacks `params.permission`. |
| 404 | `lead.notFound` | The lead doesn't exist in your organization. |
| 409 | `resource.conflict` | The change conflicts with the current state; read it again and retry. |
| 413 | `request.tooLarge` | Send a smaller body. |
| 429 | `request.rateLimited` | Wait for `Retry-After` seconds, then retry. |
| 500 | `server.internal` | Retry later. If it persists, the problem is ours. |
| 503 | `server.unavailable` | Retry with backoff. |

New codes may be added at any time; treat an unknown code by its status.
