# Errors

Every error answers with `Content-Type: application/problem+json` and the body `{type, title, status, detail, instance, code, errors?}`. The `type` is `https://app.mondochat.io/problems/<code>`.

`errors` is set for `validation`: a list of `{loc, msg}`. `reason` names the rule for `policy_denied` and `limit_reached`, and for some `forbidden` and `banned` answers, for example `guests_disabled` or `temporary`. A bad number or a NUL character in your input answers 422 `validation`, never 500.

An answer for a missing or expired token, as the API sends it:

```json
{
  "type": "https://app.mondochat.io/problems/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": null,
  "instance": "/v1/me",
  "code": "unauthorized"
}
```

`detail` is null unless the route says more, for example `token already used`. `reason` and `errors` are left out when they do not apply.

## Codes

| code | status | meaning |
|---|---|---|
| `unauthorized` | 401 | The token is missing, invalid or expired. |
| `forbidden` | 403 | You are authenticated but not allowed. `reason` can say why. |
| `banned` | 403 | The user or the IP is banned. `detail` carries the support email if set. `reason` is `temporary` when the ban expires. |
| `policy_denied` | 403 | The app settings forbid it. `reason` names the rule. |
| `limit_reached` | 403 | A daily limit was reached. `reason` names it. |
| `not_found` | 404 | Missing, or in another tenant. It is never 403 for another tenant. |
| `conflict` | 409 | The state conflicts with the request. |
| `busy` | 409 | The callee is in a call or was rung in the last 45 seconds. |
| `idempotency_conflict` | 409 | The same `Idempotency-Key` was sent with a different body. |
| `gone` | 410 | No longer available: a view-once message opened more than 60 seconds ago, or a sync cursor the server no longer serves (`reason: stale_cursor`). On `stale_cursor`, reload, then sync from the new `latest_seq`. |
| `precondition_failed` | 412 | The `If-Match` version is stale. |
| `validation` | 422 | The input is invalid. `errors` lists each field. |
| `rate_limited` | 429 | You sent too many requests. Wait `Retry-After` seconds. |
| `upstream_failed` | 502 | Your callout endpoint or a provider failed. |
| `unavailable` | 503 | The database is resuming or busy. `Retry-After: 5`. |

On `POST /v1/device/session`, a 401 `unauthorized` carries `login_url`: the `pwa.login_url` setting as set, with `{lang}` left to fill, or null.

A blocked recipient is not an error. The send succeeds for the sender and is not delivered.

## Rate limits

Rate limited routes set the headers `RateLimit-Policy` and `RateLimit`. A request over its limit answers 429 `rate_limited` with `Retry-After`.

- Per app: 50 user requests per second by default, set by `rate_limits.app_requests_per_second`. Each API server counts on its own, so the total across servers can be higher.
- Per role, in settings `limits`: `messages_per_minute` (30 by default) answers 429 `rate_limited`. `new_conversations_per_day`, `photos_per_day` (photos and videos together), `voice_per_day` (100 by default) and `messages_to_unanswered` answer 403 `limit_reached` with the limit's name as `reason`. A limit set to null is unlimited.
- A tenant in `test` status accepts at most 50 users per app. The 51st answers 403 `forbidden` with reason `test_user_limit`. Ask us to move the tenant to `live`.
- Calls ring for 45 seconds and end after 240 minutes by default (`calls.ring_timeout_seconds`, `calls.max_duration_minutes`).
- A client token may live at most 600 seconds after `iat`. A session token lasts 900 seconds. An app has at most 2 active server secrets and 20 origins, and one endpoint that answers callouts.

## Retry safely

Unsafe server to server calls take an `Idempotency-Key` header. A repeat with the same key and body within 24 hours replays the stored response. The same key with another body answers 409 `idempotency_conflict`.

## Related topics

- [Settings reference](/reference/settings/)
- [Delivery and retries](/webhooks/delivery/)
- [Webhooks overview](/webhooks/overview/)