# Errors

A successful response always has status 200. An error comes with a 4xx or 5xx status and an RFC 9457 body.

## Format

An error is returned with the `Content-Type: application/problem+json` header and a body with eight fields:

```json
{
  "type": "urn:meliora:problem:invalid_parameter",
  "title": "Invalid parameter",
  "status": 400,
  "detail": "Check the parameter value",
  "instance": "urn:request:0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20",
  "code": "invalid_parameter",
  "param": "limit",
  "request_id": "0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20"
}
```

| Field | Description |
|---|---|
| `type` | URI of the error type: `urn:meliora:problem:` followed by the code. |
| `title` | Short name in English. |
| `status` | HTTP status, the same as the response status. |
| `detail` | Human-readable explanation. The language depends on the `Accept-Language` header. |
| `instance` | URI of the request: `urn:request:` followed by the request ID. |
| `code` | Machine-readable error code. |
| `param` | The parameter the error relates to, or `null`. |
| `request_id` | Request ID, the same as in the `Request-Id` header. |

## Rejections before key validation

Some requests are rejected before the key is validated: the per-IP rate limit is exceeded, the path is unknown, the request has a body, or the service is overloaded. These responses have the same `status`, `code`, `param` and `request_id` fields, but `type` is `about:blank`, and `title` and `detail` match `code`:

```json
{
  "type": "about:blank",
  "title": "rate_limit_exceeded",
  "status": 429,
  "code": "rate_limit_exceeded",
  "detail": "rate_limit_exceeded",
  "instance": "urn:request:0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20",
  "request_id": "0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20",
  "param": null
}
```

So rely on `status` and `code`, not on `type` and `title`.

## Handling errors

- Branch your logic on `status` and `code`. The `detail` text may change, so don't parse it in code.
- If you get an unfamiliar `code`, handle the error by its HTTP status: 4xx means a problem with the request or access, 5xx means a temporary problem on our side.
- For a parameter error, `param` contains the parameter name: `limit`, `status`, `cursor`, `include_text` and so on.
- An error never advances the cursor. When you retry, a request with the same cursor returns the same data.

## Explanation language

By default, `detail` is in Russian. To get English text, send the `Accept-Language: en` header. The `code` and `title` fields don't depend on the language.

## Request-Id

Every response, including errors, has a `Request-Id` header with the request's UUID. Log it together with the endpoint and the time. When you contact [Support](https://docs.melioralab.io/en/support/), the `Request-Id` is all we need to find the request. Never send us the key itself.

## Error codes

| Status | Code | What happened |
|---|---|---|
| 400 | `invalid_parameter` | The parameter value has an invalid format, is out of range or is used in an invalid combination. The parameter name is in `param`. |
| 400 | `unknown_parameter` | The route does not accept this parameter. An API key in the URL also causes this error. |
| 400 | `cursor_mismatch` | The cursor is corrupted, was issued for a different route or filters, or the key's permissions and scope have changed. |
| 401 | `invalid_api_key` | The key is missing, invalid, revoked or expired. |
| 403 | `scope_required` | The key does not have permission for this read. Texts also require "Message text" to be enabled. |
| 403 | `api_disabled` | API access is disabled in Meliora. |
| 403 | `api_not_available` | The organization's plan does not include the API. Read access requires Pro or higher. |
| 403 | `subscription_inactive` | The organization's subscription is not active. |
| 403 | `module_not_available` | The outreach module is unavailable for this organization. |
| 403 | `organization_suspended` | Organization access is suspended. |
| 403 | `ip_not_allowed` | The request came from an IP address that is not on the key's allow list. |
| 404 | `not_found` | The object was not found or is outside the key's scope. An unknown path also returns 404. |
| 405 | `method_not_allowed` | Only the GET method is allowed. The public endpoint answers other methods with a 404 without a body, so this code is not seen from outside. |
| 410 | `cursor_expired` | The cursor expired: 15 minutes for lists, 30 days for the event feed. |
| 429 | `rate_limit_exceeded` | The request limit of the key, organization or IP address is exhausted. Retry after `Retry-After` seconds. |
| 503 | `service_unavailable` | The data could not be read right now. Retry later. |
| 503 | `server_busy` | The service is overloaded. Retry after `Retry-After` seconds. |
| 503 | `integration_initializing` | The new key is still being prepared. Retry later. |

## What to retry

| Status | Retry? | How |
|---|---|---|
| 400 | No | Fix the parameter named in `param`. For `cursor_mismatch`, restart the list. |
| 401, 403 | No | Check the key, its permissions and your organization settings. |
| 404 | No | The object doesn't exist or is outside the key's scope. |
| 410 | No | The cursor expired: restart the list or [recover the feed](https://docs.melioralab.io/en/events-polling/#vosstanovlenie). |
| 429 | Yes | Wait `Retry-After` seconds and add random jitter. |
| 503 | Yes | Wait `Retry-After` seconds and increase the delay if the error repeats. |

For more on delays between retries, see [Rate limits and retries](https://docs.melioralab.io/en/rate-limits/).

## Special cases

- **An unknown path** or an object ID that isn't a valid UUID returns 404 `not_found`. Write the path exactly as in the reference: a trailing `/` or encoded characters in the path also return 404.
- **Methods other than GET**, including `HEAD` and `OPTIONS`, and URLs outside `/v1/` get 404 with no body. You won't see the `method_not_allowed` code from outside.
- **A body in a GET request**, a URL longer than 8 KiB or headers larger than 8 KiB return 400 `invalid_parameter` with `param` set to `body`, `uri` or `headers`.
- **A repeated parameter** (`?status=new&status=replied`) and **an empty value** (`?status=`) return 400 `invalid_parameter`.
- **A long-running request** is aborted after 10 seconds with 503 `service_unavailable`.
