# Errors

Source: https://developers.leonai.io/docs/errors/

The API uses conventional HTTP status codes and returns a consistent JSON error
envelope for every non-2xx response.

## Error envelope

```json
{
  "error": "Not Found",
  "message": "Link not found",
  "statusCode": 404,
  "details": null,
  "timestamp": "2026-07-10T12:00:00.000Z",
  "path": "/api/v1/links/nope",
  "requestId": "req-abc123"
}
```

| Field        | Type         | Always present | Meaning                                                                  |
| ------------ | ------------ | -------------- | ------------------------------------------------------------------------ |
| `error`      | string       | yes            | Short error label (e.g. `Validation Error`, `Not Found`, `Unauthorized`) |
| `message`    | string       | yes            | Human-readable explanation                                               |
| `statusCode` | number       | yes            | The HTTP status code                                                     |
| `details`    | array/object | no             | Extra context — present on validation errors (see below)                 |
| `timestamp`  | string       | yes            | ISO-8601 time the error was generated                                    |
| `path`       | string       | yes            | The request path                                                         |
| `requestId`  | string       | usually        | Correlation id — include it in support requests                          |

:::note[SDK mapping]
The planned [TypeScript SDK](https://developers.leonai.io/docs/sdks/) (not published yet) will throw an `APIError` whose `code` maps to the API's
`error` label, `message` to `message`, and `details` to `details`. It also exposes
`status`, `rateLimit`, and `retryAfter`.
:::

## Validation errors

`400` validation failures include a `details` array, one entry per invalid field:

```json
{
  "error": "Validation Error",
  "message": "Request validation failed",
  "statusCode": 400,
  "details": [
    {
      "field": "longUrl",
      "message": "Invalid URL format (only http/https allowed)",
      "code": "custom"
    }
  ]
}
```

## Status codes

| Status | `error` label                      | When                                                    |
| ------ | ---------------------------------- | ------------------------------------------------------- |
| `400`  | `Validation Error` / `Bad Request` | Invalid input, malformed body, bad reference            |
| `401`  | `Unauthorized`                     | Missing or invalid API key / JWT                        |
| `403`  | `Forbidden`                        | Authenticated but lacking scope, or tier limit reached  |
| `404`  | `Not Found`                        | Resource (or route) does not exist                      |
| `409`  | `Conflict`                         | Uniqueness violation (e.g. a short code already exists) |
| `429`  | `Too Many Requests`                | Rate limit exceeded — see `Retry-After`                 |
| `500`  | `Internal Server Error`            | Unexpected server error                                 |

## Rate-limit errors

`429` responses include a `Retry-After` header (seconds) alongside the standard
`X-RateLimit-*` headers. The planned SDK will retry idempotent requests automatically and
honor `Retry-After`; see [Authentication → Rate limiting](https://developers.leonai.io/docs/authentication/#rate-limiting).

## Handling errors well

- **Read `error` + `message`**, not just the status code — the message explains the
  specific cause.
- **Log `requestId`** so support can trace a failed call.
- **Treat `403` from tier limits as an upgrade prompt**, not a bug — the message will
  say which limit was hit.
- **Make retries idempotent.** The planned SDK will never replay POST/PATCH; if you retry
  writes yourself, use an idempotency strategy (e.g. `externalId` for conversions).
