# Errors

> Read the error code, then take the matching recovery action.

## Error envelope

Read `error.code` for recovery logic. Show `error.message` to a developer.

```json title="JSON"
{
  "error": {
    "code": "tool_result_invalid",
    "message": "Tool result failed schema validation.",
    "request_id": "request_123",
    "details": [{"path": "/events/0/content/title", "reason": "is shorter than minLength"}]
  }
}
```

## Error codes

| Error code                   | Meaning                                                                                                                        |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `invalid_request`            | HTTP 400 or 422. Fix the request body or parameter.                                                                            |
| `invalid_cursor`             | HTTP 400. Restart replay without the bad cursor.                                                                               |
| `authentication_required`    | HTTP 401. Send a valid bearer key.                                                                                             |
| `permission_denied`          | HTTP 403. [Create a key](https://pantheon-todo.alethiconsulting.com/docs/get-a-key) with the required scope.                   |
| `resource_not_found`         | HTTP 404. Check the id and workspace.                                                                                          |
| `idempotency_conflict`       | HTTP 409. Reuse a key only with the same payload.                                                                              |
| `event_already_resolved`     | HTTP 409. Do not execute the tool again.                                                                                       |
| `event_expired`              | HTTP 409. Start a new run.                                                                                                     |
| `run_not_resumable`          | HTTP 409. Read the run status. Start a new run if it is terminal.                                                              |
| `SESSION_BUSY`               | HTTP 409. Wait, cancel, or resume the active run.                                                                              |
| `RUN_NOT_CANCELLABLE`        | HTTP 409. Treat the run as already terminal.                                                                                   |
| `tool_input_invalid`         | HTTP 422. Fix the tool input schema or prompt.                                                                                 |
| `tool_result_invalid`        | HTTP 422. Return an object matching the tool output schema.                                                                    |
| `RATE_LIMITED`               | HTTP 429. Wait for Retry-After before retrying. See [Usage and limits](https://pantheon-todo.alethiconsulting.com/docs/usage). |
| `runtime_resume_unavailable` | HTTP 503. Retry with the same idempotency key.                                                                                 |
| `SANDBOX_PROVISION_FAILED`   | HTTP 503. Retry later or remove an unnecessary sandbox requirement.                                                            |

## Validation errors

`invalid_request` returns HTTP 400 or 422.

The response body carries an `error.details` list. Each entry has these fields:

- `path`: A JSON pointer style path, such as `/body/password` or `/body/organization_name`.
- `reason`: The rule that failed.

```json title="JSON"
{"error":{"code":"invalid_request","message":"Validation failed for the request payload.","request_id":"req_996dfa0f5eb649ee","details":[{"path":"/body/password","reason":"String should have at least 10 characters"}]}}
```

Show `details` to the user instead of the generic message, as the dashboard does.

## SESSION_BUSY

```json title="HTTP 409"
{
  "error": {
    "code": "SESSION_BUSY",
    "message": "Session 'session_123' has an active run '0123456789abcdef0123456789abcdef' in status 'waiting_for_input'.",
    "request_id": "request_123",
    "active_run_id": "0123456789abcdef0123456789abcdef",
    "active_run_status": "waiting_for_input",
    "recovery": [
      "cancel the active run via POST /v1/sessions/session_123/runs/0123456789abcdef0123456789abcdef/cancel",
      "or resume it via POST /v1/sessions/session_123/runs/0123456789abcdef0123456789abcdef/resume"
    ]
  }
}
```

Read [Sessions and runs](https://pantheon-todo.alethiconsulting.com/docs/sessions-and-runs) for the recovery flow.

## SANDBOX_PROVISION_FAILED

```json title="HTTP 503"
{
  "error": {
    "code": "SANDBOX_PROVISION_FAILED",
    "message": "Sandbox provider quota is exhausted. Please retry later.",
    "request_id": "request_123"
  }
}
```

## RATE_LIMITED

Tenant requests share three independent 60-second limits: 30 sessions, 60 runs, and 120 events.

HTTP 429 includes a `Retry-After` header with the wait in seconds.

```json title="HTTP 429, Retry-After: 12"
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Tenant rate limit reached.",
    "request_id": "request_123",
    "details": {"limit": 30, "window_seconds": 60, "retry_after": 12}
  }
}
```

`details.limit` is the configured request limit. `details.window_seconds` is the window length in seconds.

`details.retry_after` is the wait before retrying, matching the header.

See [Usage and limits](https://pantheon-todo.alethiconsulting.com/docs/usage#limits) for affected operations. Request a higher limit through [Support](https://pantheon-todo.alethiconsulting.com/docs/support#limits).

## Recovery actions

| Symptom         | Action                                                          |
| --------------- | --------------------------------------------------------------- |
| 409 busy        | Use `active_run_id`. Resume or cancel that run.                 |
| 503 sandbox     | Back off. Check quota. Disable sandboxing only when safe.       |
| 401             | Send `Authorization: Bearer PANTHEON_API_KEY`.                  |
| 422 tool result | Validate the result against the registered output schema.       |
| Stream drops    | Reconnect with the last durable SSE id. Handle `stream.resync`. |

## Pitfalls

> **Warning**
>
> - **Error code casing differs:** Use the exact code returned by the API. The contract contains lowercase and uppercase codes.
> - **A 422 leaves a run waiting:** Correct the result and retry with the same idempotency key before the deadline.
