# Errors

RFC 7807 problem details, common status codes, and how to interpret error bodies.

All non-2xx responses are served as `application/problem+json` and follow [RFC 7807 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807).

## Envelope

```json
{
  "type": "https://api.benjipays.com/problems/<slug>",
  "title": "<short title>",
  "status": 401,
  "detail": "<human-readable explanation>",
  "instance": "/v2/<path>",
  "request_id": "5e3e7c8e-1a04-4f7e-9c1f-9e0c5b9b1a23",
  "correlation_id": "trace-abc-123"
}
```

| Field            | Type         | Description                                                              |
| ---------------- | ------------ | ------------------------------------------------------------------------ |
| `type`           | string (URI) | Identifies the problem class. Stable per error category.                 |
| `title`          | string       | Short, human-readable summary of the class.                              |
| `status`         | integer      | HTTP status code.                                                        |
| `detail`         | string       | Explanation specific to this occurrence.                                 |
| `instance`       | string (URI) | URI identifying this specific failure.                                   |
| `request_id`     | string       | Echoed `X-Request-ID` — included whenever the middleware has set it.     |
| `correlation_id` | string       | Echoed `X-Correlation-ID` — included whenever the middleware has set it. |

Both `request_id` and `correlation_id` are always set by the middleware, so a single ID will identify a failure across your logs, the API's logs, and any support ticket. See [Request Tracing](./request-tracing.md).

## Common statuses

| Status                      | When                                                                                                                                                 |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`           | Validation error. May include an `errors[]` extension listing the offending fields.                                                                  |
| `401 Unauthorized`          | Missing, invalid, or expired credentials; or organization API access is not active (disabled or missing). See [Authentication](./authentication.md). |
| `403 Forbidden`             | Wrong actor type, missing OAuth scope, insufficient mapping permission for the API key owner, or free trial ended without billing enabled.           |
| `404 Not Found`             | Unknown route or tenant-scoped resource not visible to the caller.                                                                                   |
| `409 Conflict`              | Idempotency-key body conflict, or a domain conflict. See [Idempotency](./idempotency.md).                                                            |
| `429 Too Many Requests`     | Rate limit exceeded, or an in-flight idempotent request with the same key. Includes `Retry-After`. See [Rate Limits](./rate-limits.md).              |
| `500 Internal Server Error` | Unexpected failure. Safe to retry idempotent requests.                                                                                               |

## Examples

### 400 with field-level details

```json
{
  "type": "https://api.benjipays.com/problems/validation",
  "title": "Validation error",
  "status": 400,
  "detail": "Request body failed validation",
  "instance": "/v2/partners",
  "errors": [
    { "path": ["body", "externalId"], "message": "Required" }
  ],
  "request_id": "9c6d…",
  "correlation_id": "trace-abc-123"
}
```

### 401 unauthorized

```http
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
```

```json
{
  "type": "https://api.benjipays.com/problems/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing authentication token",
  "instance": "/v2/whoami"
}
```

### 403 forbidden — wrong actor

```json
{
  "type": "https://api.benjipays.com/problems/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Access denied. Required actor type: partner",
  "instance": "/v2/partners/accounts"
}
```

### 403 forbidden — missing scope

```json
{
  "type": "https://api.benjipays.com/problems/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Insufficient scope",
  "instance": "/v2/partners/accounts"
}
```

### 403 forbidden — free trial ended without billing

Organization-scoped merchant routes (`/v2/invoices`, `/v2/gateways`, etc.) return this when the organization's free trial has ended and billing is not enabled:

```json
{
  "type": "https://api.benjipays.com/problems/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Free trial has ended and billing is not enabled",
  "instance": "/v2/gateways"
}
```

### 429 too many requests

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 1
```

```json
{
  "type": "https://api.benjipays.com/problems/too_many_requests",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Please retry after the specified time.",
  "instance": "/v2/whoami",
  "retry_after_seconds": 1
}
```

## Best practices

* Parse `application/problem+json` responses by their `status` and `type` — don't string-match on `detail`.
* Distinguish 401 (re-authenticate) from 403 (you can't do this with these credentials).
* Use `request_id` / `correlation_id` from the body when filing support tickets.
* Retry `429` only after honoring `Retry-After`. Retry `500` only on idempotent operations.
* Treat unknown problem `type` URIs as if they were just the HTTP `status` — the platform may add new categories.