# Errors

> Mojeeb Public API error envelope — type, code, message, correlation_id — with the full catalog of error codes, HTTP statuses, and meanings.

Every error from the Mojeeb Public API uses the same envelope. Branch on `code` (stable, forever-additive) — never on `message` (human-readable, may be reworded without notice).

## What does an error response look like?

```json
{
  "error": {
    "type":           "validation_error",
    "code":           "invalid_from_phone",
    "message":        "The 'from' field must be an E.164 phone number...",
    "param":          "from",
    "correlation_id": "req_01KQ..."
  }
}
```

| Field | Always present? | Purpose |
|---|---|---|
| `type` | Yes | Stable category for catch blocks |
| `code` | Yes | Stable, specific reason — branch on this |
| `message` | Yes | Human-readable; **never use as a key** |
| `correlation_id` | Yes | Trace id for our logs — quote in support tickets |
| Extra fields | Sometimes | Per-error context (e.g. `param`, `available_phones`, `original_correlation_id`) |

## What are the error types?

A small, stable set of categories. Forever-additive — new types may appear, none will be removed:

| `type` | Typical HTTP |
|---|---|
| `authentication_error` | 401 |
| `permission_error` | 403 |
| `validation_error` | 413, 422 |
| `idempotency_error` | 409 / 422 |
| `rate_limit_error` | 429 |
| `quota_error` | 402 |
| `conflict_error` | 409 |
| `not_found_error` | 404 |
| `internal_error` | 500, 503 (and 401 when the key couldn't be checked) |

## What's the full code catalog?

Codes are a stable enum. New codes appear without notice; nothing existing will be renamed or repurposed.

| `code` | HTTP | Meaning |
|---|---|---|
| `invalid_api_key` | 401 | Missing, malformed, unknown, revoked or expired key. Fix your `Authorization` header. |
| `insufficient_scope` | 403 | Key lacks the required scope (response includes `required_scope`). |
| `agent_not_authorized` | 403 | Key is restricted to specific agents and this one isn't on its list. |
| `shared_with_other_agents` | 403 | The knowledge base, or a custom API tool's site login, is also used by agents the key can't act on, so the key can't change it. |
| `feature_not_available` | 403 | Your plan doesn't include this: custom API tools are on paid plans. Upgrade, or use the endpoints your plan has. |
| `not_note_author` | 403 | Only a note's author can edit or delete it; a key acts as the person who created it. |
| `system_note_read_only` | 403 | Notes Mojeeb records, such as lead status changes, can't be edited or deleted. |
| `agent_id_required` | 403 | A key limited to some agents called a WhatsApp endpoint without a valid `agent_id`. |
| `agent_not_found` | 404 | The agent isn't one of your organization's agents. |
| `not_found` | 404 | The conversation, contact, note, lead, lead field, knowledge base, connection, custom API tool or comment isn't the agent's (a deleted conversation is not found too), a conversation's message you asked for isn't one of its messages, or the connection whose templates you asked for isn't a WhatsApp number. On the usage history, your organization has no subscription. |
| `message_not_found` | 404 | `GET /v1/whatsapp/messages/{id}`: the id doesn't exist, belongs to another organization, or was sent through an agent outside the key's agent list. |
| `endpoint_not_found` | 404 | No `/v1/` endpoint matches the method and path, including an id that isn't a UUID. Check for typos. |
| `lead_capture_not_found` | 404 | The agent has no Capture Lead tool, so it has no lead capture prompt to read or change. Contact Mojeeb support to have the tool added. |
| `invalid_request_body` | 422 | A field is missing, has the wrong type, breaks a limit or a format (such as a lead `phone` or contact `mobile` that isn't E.164), or a query parameter's value isn't valid (response includes `param` naming it). |
| `invalid_request` | 422 | A business rule refused the change (for example an unknown lead status or a reserved lead field key), the `cursor` isn't one the API returned, or a message list's `after` isn't a message of the conversation or was sent together with `cursor` (both with `param: after`). The `message` says which. |
| `invalid_date_range` | 422 | The analytics window is out of bounds: `to` is before `from`, a date is later than today in UTC, or the window is longer than the endpoint allows. The `message` says which. |
| `no_settings` | 422 | A settings or conversation update sent nothing to change. |
| `messaging_window_closed` | 422 | A reply in a conversation whose customer hasn't written in the last 24 hours: WhatsApp, Facebook and Instagram won't deliver it. Nothing was sent. On WhatsApp, send a template. |
| `channel_not_supported` | 422 | A reply in a conversation on a channel Mojeeb can't send on yet (TikTok). Nothing was sent. |
| `comment_already_hidden` | 422 | `hideComment` on a comment that is hidden already. |
| `comment_not_hidden` | 422 | `unhideComment` on a comment that isn't hidden. |
| `platform_hide_failed` | 422 | Facebook or Instagram refused to hide or show the comment, for example because it was deleted, or couldn't be reached. It can be temporary. |
| `field_not_writable` | 422 | An agent update sent a value for a field a key can't change: `status` (so a key can't delete an agent), `ai_model`, `model_provider`, `avatar_url` or `language` (response includes `param` naming it). Nothing was changed. |
| `unknown_timezone` | 422 | `time_zone` isn't an IANA time zone id in Region/City form, such as `Africa/Cairo` (UTC is `Etc/UTC`). |
| `invalid_follow_up_platform` | 422 | A follow-up channel isn't `whatsapp`, `facebook` or `instagram`. |
| `invalid_follow_up_steps` | 422 | More steps than allowed, a delay out of range, or two steps with the same delay. |
| `assignee_not_allowed` | 422 | The teammate can't open the agent (they aren't in your organization, or they're limited to other agents), so the lead or conversation can't be assigned to them. Pick someone with `can_open_agent: true` on the agent's members list. |
| `system_field` | 422 | A built-in lead field (`built_in: true`), the lead statuses included, can't be changed or deleted through the API. |
| `lead_field_limit_reached` | 422 | The agent already has as many custom lead fields as the API can add. Delete one it no longer needs first. |
| `connection_inactive` | 409 | The connection is disconnected (`active: false`), so its settings can't be changed and its templates can't be listed. Reconnect it in the dashboard first. |
| `connection_needs_reconnection` | 409 | Meta no longer accepts the WhatsApp number's access (it was revoked or expired, or the number left its WhatsApp Business Account), so its templates can't be read. Reconnect the number in the dashboard; retrying won't help. |
| `field_key_taken` | 409 | The agent already has a lead field with this key, built-in fields included. Nothing was created. A create retried after a lost response lands here too, so list the lead fields and carry on. |
| `no_current_usage_period` | 409 | No billing period of your subscription covers now, so Mojeeb treats it as not renewed and the AI doesn't reply until a period opens. It clears when the renewal lands; if it lasts, contact support with the `correlation_id`. `GET /v1/organization/usage` also answers it when your organization has no subscription at all. |
| `usage_periods_overlap` | 409 | More than one billing period of your subscription covers now, so Mojeeb can't tell which limits apply. Contact support with the `correlation_id`. |
| `login_shared` | 409 | The custom API login you sent differs from the site's saved one, or carries a secret, and other tools use it. Send `change_shared_login: true` to change it for all of them, or leave `login` out. |
| `tool_not_tested` | 409 | A custom API tool can be switched on only after a passing test of its current request. Test it, then switch it on. |
| `request_body_too_large` | 413 | The request body is larger than 1 MB. |
| `invalid_agent_id` | 422 | `agent_id` isn't a valid UUID (WhatsApp endpoints). |
| `invalid_phone_number` | 422 | `to` isn't a valid E.164 number (WhatsApp endpoints). A lead `phone` or contact `mobile` that isn't E.164 is `invalid_request_body`. |
| `invalid_from_phone` | 422 | `from` isn't a valid E.164 number. |
| `from_phone_not_found_for_agent` | 422 | `from` is well-formed but doesn't match any active WhatsApp connection on the agent (response includes `available_phones`). |
| `template_header_media_url_invalid` | 422 | `template.header_media_url` isn't an absolute HTTPS URL. |
| `template_not_found` | 422 | The connection has no template with that name, whatever its status (response includes `available_templates`). When Mojeeb can't read the templates from Meta, or the number has more templates than Mojeeb reads and this one isn't among them, this check is skipped and the send is accepted. |
| `template_not_found_for_language` | 422 | The template has no version in that language, whatever its status (response includes `available_languages`). |
| `idempotency_key_in_progress` | 409 | A request with this key is still running, or one with it that was refused or failed after claiming the key (for example a reply's `messaging_window_closed`) started less than 60 seconds ago, whatever the body. Retry after `Retry-After`, or send the corrected request with a new key. |
| `idempotency_key_in_use_with_different_params` | 422 | The key belongs to an earlier accepted request that differs from this one: another body, or another endpoint or conversation (the same text replied in another conversation counts as different). The response includes `original_correlation_id`. |
| `idempotency_unknown_outcome` | 500 | An internal fault in the idempotency check (not expected). Retry with the same key and body. |
| `unknown_send_outcome` | 500 | An internal fault mapping the send result (not expected). Look up the message before sending again. |
| `rate_limit_exceeded` | 429 | Per-key rate limit hit: the default one, or the lower one of the WhatsApp template list or of replies in a conversation. Back off and retry after `Retry-After`. |
| `test_rate_limited` | 429 | Your company ran too many custom API tool tests this minute (30, across its keys and the dashboard). Retry after `Retry-After`. |
| `message_limit_exceeded` | 402 | Reserved: the subscription's message limit. Not sent today. |
| `access_unavailable` | 503 | The access check couldn't run: the key's, or when assigning a lead or a conversation, the teammate's. Retry with backoff. |
| `meta_unavailable` | 503 | Meta didn't return what was asked, such as a WhatsApp number's templates (an outage, a timeout or Meta's own rate limit). Retry with backoff. |
| `internal_server_error` | 500, 401 | Our problem: retry with exponential backoff, and if it persists, send us the `correlation_id`. With 401 it means we couldn't check the key; retry, don't rotate it. |

## What HTTP statuses can the API return?

| Status | Meaning |
|---|---|
| `200` | Success with a body (a read, an update or an assignment) |
| `201` | Resource created (a lead, a note, a knowledge base, a lead field, a custom API tool, a reply in a conversation) |
| `202` | Accepted for async processing (a message queued for send) |
| `204` | Success with no body (a delete) |
| `401` | Authentication failed |
| `402` | Reserved: quota exceeded |
| `403` | Authorization failed (scope, agent list, shared knowledge or login, note author, a feature your plan doesn't include) |
| `404` | Resource not found |
| `409` | Conflict (idempotency key in progress, a disconnected connection or one that needs reconnecting, a lead field key that already exists, no single current billing period, a shared custom API login, or an untested tool) |
| `413` | Request body larger than 1 MB |
| `415` | The body isn't sent as `application/json` (a standard problem-details body, not the envelope) |
| `422` | Validation error, including a body that isn't valid JSON |
| `429` | Rate limit exceeded |
| `500` | Internal error |
| `503` | The access check or Meta temporarily unavailable |

## Which errors should I retry?

Retry only the temporary ones. Wait for `Retry-After` on `429` and on `409 idempotency_key_in_progress`. Use exponential backoff on `503` errors, on `500` errors except `unknown_send_outcome`, on `422 platform_hide_failed` (Facebook or Instagram may just be unavailable), and on a `401` whose code is `internal_server_error`. Every other error needs a change before you send the request again.

| Error | What to do |
|---|---|
| `409 connection_inactive`, `409 connection_needs_reconnection` | Reconnect the connection in the dashboard, then try again. |
| `409 field_key_taken` | The field already exists: list the agent's lead fields and carry on. |
| `409 no_current_usage_period` | Wait: it clears when the renewal lands. Contact support if it lasts. With no subscription at all it won't clear until you subscribe. |
| `409 usage_periods_overlap` | Contact support with the `correlation_id`. |
| `409 login_shared` | Leave `login` out, or send `change_shared_login: true` if every tool on the site should change. |
| `409 tool_not_tested` | Test the tool, then switch it on in a request of its own. |
| `500 unknown_send_outcome` | Look up the message before sending it again. |
| `422 messaging_window_closed` | Wait for the customer to write, or, on WhatsApp, send a template. |
| `422 platform_hide_failed` | Try again later, with backoff. If it keeps failing, the comment was probably deleted. |
| `422 comment_already_hidden`, `422 comment_not_hidden` | Nothing to fix: the comment is already as you asked. After a `500`, it means your first request went through. |
| Any other `4xx` | Fix the request; the `code` and `message` say what to change. |

## How do I use the correlation_id?

**Save it on every error.** When you contact support, paste the `correlation_id` from the failing response — we can trace the entire request lifecycle in our logs in seconds.

You can also send your own trace id and we'll honor it when it's valid:

```bash
curl ... -H "X-Correlation-ID: my-app-trace-abc123"
```

Up to 128 characters: letters, digits, `_` and `-`. An id that breaks these rules is ignored without an error and we generate one instead, so take the id from the response header rather than assuming yours was kept. We reflect the id in `X-Correlation-ID` on the response and embed it in any error body. If you don't send one, we generate `req_<26-char ULID>`.

## Common questions

### Why does `invalid_api_key` cover so many sub-cases?

A single code keeps client logic simple — your code only needs one branch for "auth failed." The `message` field disambiguates for humans debugging, but you should never branch on text. Treat any `invalid_api_key` as "fix the key and retry."

### Will you add new error codes later?

Yes. New `code` values can appear without notice as we add features. Keep your switch/match statements default-safe — fall back to "unknown error, log and surface to user."

### What about field-level errors with multiple invalid fields?

Currently the API returns the first validation error encountered. We may add a richer multi-error shape later as an additive change (a new optional field on the envelope, never replacing existing fields). For now, fix one error at a time.

### What's `original_correlation_id`?

Returned on `idempotency_key_in_use_with_different_params`. It points to the trace id of the request that originally claimed the idempotency key, so you can find that earlier request in your own application logs.

### What's `available_phones`?

Returned on `from_phone_not_found_for_agent`. Lists every active WhatsApp number on the agent in E.164 format, so you can self-correct your `from` value without a second roundtrip to discover what was valid.
