> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Errors

Jelliu uses conventional HTTP status codes and returns every error in the same JSON envelope:

```json
{
  "error": {
    "code": "CAMPAIGN_NOT_ACTIVE",
    "message": "Campaign can only be activated from DRAFT or PAUSED status"
  }
}
```

| Field            | Always present              | Description                                                               |
| ---------------- | --------------------------- | ------------------------------------------------------------------------- |
| `error.code`     | Yes                         | Stable, machine-readable identifier. **Branch on this.**                  |
| `error.message`  | Yes                         | Human-readable explanation. May change at any time.                       |
| `error.details`  | Only on `VALIDATION_FAILED` | Which fields failed and why. See [Validation errors](#validation-errors). |
| `error.metadata` | Only on a few codes         | Structured context, such as plan limits. See [Metadata](#metadata).       |

## Messages are for people, codes are for code

Messages are written for the person reading them, and the language depends on the endpoint. Jelliu's customers are Spanish-speaking, so many product-facing messages are in **Spanish**, while others are in English. Both of these are real responses:

```json
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Servicio no disponible temporalmente. Vuelve a intentarlo en unos segundos."
  }
}
```

```json
{
  "error": {
    "code": "BILLING_ERROR",
    "message": "Campaign limit reached (3 campaigns on the growth plan). Upgrade your plan in Settings → Plan.",
    "metadata": { "limit": 3, "current": 3, "tier": "growth" }
  }
}
```

Never parse or match `message`. It is not translated consistently and can be reworded without notice. Switch on `error.code` and the HTTP status.

## HTTP status codes

| Status | Meaning                                                                                           |
| ------ | ------------------------------------------------------------------------------------------------- |
| `400`  | The request is malformed or failed validation.                                                    |
| `401`  | Missing, malformed, unknown, revoked or expired API key.                                          |
| `402`  | A plan limit was exceeded (`PLAN_LIMIT_EXCEEDED` on some routes).                                 |
| `403`  | Authenticated but not allowed: scope, role, plan feature, compliance rule or suspended workspace. |
| `404`  | The resource does not exist or belongs to another workspace.                                      |
| `409`  | The request conflicts with the resource's current state.                                          |
| `422`  | The request is well-formed but semantically invalid (used by a few endpoints).                    |
| `429`  | Rate limit, concurrency limit or budget exhausted. Honor `Retry-After`.                           |
| `500`  | Unexpected server error.                                                                          |
| `502`  | An upstream provider failed (for example an integration sync).                                    |
| `503`  | A temporary infrastructure problem. Safe to retry after `Retry-After`.                            |

For `5xx` responses the real cause is logged on our side and never returned. The body carries a generic message: `Internal server error`, or `An unexpected error occurred` with code `INTERNAL_ERROR`.

## Common error codes

The status shown is the usual one. A few codes are returned with a different status on specific endpoints (for example `VALIDATION_FAILED` with `409` or `422`), so read the status and the code together.

| Code                                                                                | Status        | When                                                                                                                         |
| ----------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_FAILED`                                                                 | 400           | Body, query or path parameters failed validation.                                                                            |
| `UNAUTHORIZED`                                                                      | 401           | See [Authentication](/authentication#401-unauthorized).                                                                      |
| `FORBIDDEN`                                                                         | 403           | The key's scope or the route's role requirement does not allow the request.                                                  |
| `TENANT_SUSPENDED`                                                                  | 403           | The workspace is suspended.                                                                                                  |
| `BILLING_ERROR`                                                                     | 403 (usually) | The plan does not include the feature or channel, or a resource cap (agents, campaigns, contacts, integrations) was reached. |
| `COMPLIANCE_BLOCKED`                                                                | 403           | Blocked by compliance rules or content screening.                                                                            |
| `PHONE_NUMBER_REQUIRED`                                                             | 403           | Outbound calling needs a phone number owned by the workspace.                                                                |
| `NOT_FOUND`                                                                         | 404           | Generic not found.                                                                                                           |
| `AGENT_NOT_FOUND`, `CAMPAIGN_NOT_FOUND`, `CALL_NOT_FOUND`, `CONVERSATION_NOT_FOUND` | 404           | The specific resource was not found in your workspace.                                                                       |
| `CAMPAIGN_NOT_ACTIVE`                                                               | 400 or 409    | The campaign is not in a state that allows the transition.                                                                   |
| `PLAN_LIMIT_EXCEEDED`                                                               | 402 or 409    | A hard limit was reached, such as 25 active API keys.                                                                        |
| `CALL_ALREADY_IN_PROGRESS`                                                          | 409           | The same operation is already running, for example a number being provisioned. Retry shortly.                                |
| `AGENT_PROVISIONING`                                                                | 409           | The agent is still being set up. Retry after `Retry-After` (5 seconds).                                                      |
| `AGENT_PAUSED`                                                                      | 409           | The agent has been stopped in the dashboard. `503` if its state could not be checked.                                        |
| `AGENT_CHANGE_PENDING_APPROVAL`                                                     | 409           | The change was held for review and **not** applied.                                                                          |
| `AGENT_ACTION_PENDING_APPROVAL`                                                     | 409           | The action is waiting for a person to approve it and did **not** run.                                                        |
| `AGENT_ACTION_APPROVAL_CLOSED`                                                      | 409           | The approval request already expired or was decided.                                                                         |
| `NUMBER_UNVERIFIED`                                                                 | 400           | The telephony account can only call verified numbers.                                                                        |
| `RATE_LIMIT_EXCEEDED`                                                               | 429           | See [Rate limits](/rate-limits).                                                                                             |
| `MAX_CONCURRENT_CALLS_REACHED`                                                      | 429           | The concurrency limit for calls was reached.                                                                                 |
| `AGENT_BUDGET_EXCEEDED`                                                             | 429           | The agent used up the action or spend budget set by the workspace.                                                           |
| `AI_WEEKLY_CAP_REACHED`                                                             | 429           | The weekly budget for an in-product AI feature is used up.                                                                   |
| `INTEGRATION_SYNC_FAILED`                                                           | 502 (usually) | A connected integration or upstream provider failed.                                                                         |
| `INTERNAL_ERROR`                                                                    | 500           | Unexpected error.                                                                                                            |
| `SERVICE_UNAVAILABLE`                                                               | 503           | Temporary infrastructure problem. `Retry-After: 5`.                                                                          |

## Validation errors

Validation failures return `400` with code `VALIDATION_FAILED` and a `details` field. Depending on the endpoint, `details` takes one of two shapes.

**Field map.** Most create and update endpoints report errors grouped by field:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Invalid campaign input",
    "details": {
      "formErrors": [],
      "fieldErrors": {
        "agentId": ["Invalid uuid"],
        "productContext": ["String must contain at least 10 character(s)"]
      }
    }
  }
}
```

**Issue list.** Other endpoints return a flat list of issues, each with a dotted `path`:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request validation failed",
    "details": [
      { "path": "url", "message": "Invalid url", "code": "invalid_string" }
    ]
  }
}
```

Handle both: if `details` is an array, read `path` and `message` from each item; if it is an object, read `fieldErrors`.

## Metadata

`metadata` is included only for the four codes below, and only with keys from a fixed allowlist. Everything else stays server-side. The keys you will typically see:

| Code                            | Typical keys                                            |
| ------------------------------- | ------------------------------------------------------- |
| `BILLING_ERROR`                 | `limit`, `current`, `tier`, `upgradeTo`                 |
| `AI_WEEKLY_CAP_REACHED`         | `kind`, `tier`, `weekStart`, `limitTokens`, `upgradeTo` |
| `AGENT_CHANGE_PENDING_APPROVAL` | `changeRequestId`, `fields`                             |
| `AGENT_ACTION_PENDING_APPROVAL` | `requestId`, `expiresAt`, `reversibility`, `match`      |

## Retrying

* Retry `429` and `503` after the number of seconds in the `Retry-After` header.
* `409 AGENT_PROVISIONING` also carries `Retry-After`.
* Other `4xx` errors will fail again until the request, the key or the resource state changes.
* For other `5xx` errors, retry with exponential backoff.