> ## Documentation Index
> Fetch the complete documentation index at: https://docs.causeloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Error response shape, HTTP status codes, error codes, and how to handle common errors.

The Causeloop API uses a consistent error envelope for all failure responses. Every error is JSON with a top-level `error` object.

## Error response shape

```json theme={null}
{
  "error": {
    "code": "not_found",
    "message": "Issue iss_01abc123 not found",
    "trace_id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "details": {
      "id": "iss_01abc123"
    }
  }
}
```

<ResponseField name="error.code" type="string" required>
  Machine-readable error code. Use this field for programmatic error handling.
</ResponseField>

<ResponseField name="error.message" type="string" required>
  Human-readable description of the error. Do not rely on this string in code — it may change. Use `code` instead.
</ResponseField>

<ResponseField name="error.trace_id" type="string" required>
  Unique correlation ID for this request. Include this value when contacting support — it links to logs, audit events, and the outbound `X-Request-Id` response header.
</ResponseField>

<ResponseField name="error.details" type="object">
  Optional map of field-level error messages. Present on validation errors to pinpoint which input field failed and why.
</ResponseField>

## HTTP status codes

| Status | Meaning                                                                                 |
| ------ | --------------------------------------------------------------------------------------- |
| `400`  | Bad request — malformed body, invalid query parameter, or validation error.             |
| `401`  | Unauthorized — missing, invalid, or expired `Authorization` token.                      |
| `403`  | Forbidden — authenticated but lacking the required scope or permission.                 |
| `404`  | Not found — the resource does not exist or is not visible to this token.                |
| `409`  | Conflict — the operation conflicts with existing state (e.g. duplicate name).           |
| `422`  | Unprocessable entity — the request was well-formed but semantically invalid.            |
| `429`  | Too many requests — rate limit exceeded. See [Rate Limits](/api-reference/rate-limits). |
| `500`  | Internal server error — unexpected error. Retry with exponential backoff.               |

## Error codes

| Code                       | Status | Description                                                                                                                                                                                                                                                         |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error`         | 400    | Request body or query params failed validation. Check `details` for field-level messages.                                                                                                                                                                           |
| `idempotency_key_required` | 400    | A mutating request to `/v1/ingest/*`, `/v1/configs/*`, or `/v1/engine/runs` was sent without an `Idempotency-Key` header. Mandatory on these paths — see [Idempotency](/api-reference/idempotency#mandatory-idempotency-keys).                                      |
| `too_many_records`         | 400    | `POST /v1/ingest/batch` exceeded the 500-record limit.                                                                                                                                                                                                              |
| `unauthorized`             | 401    | Missing or invalid `Authorization` header, or expired token.                                                                                                                                                                                                        |
| `invalid_grant`            | 401    | The `subject_token` passed to `/auth/exchange` is invalid or could not be verified.                                                                                                                                                                                 |
| `forbidden`                | 403    | Valid token but insufficient scope for this operation.                                                                                                                                                                                                              |
| `mfa_required`             | 403    | MFA enrollment is required by this workspace.                                                                                                                                                                                                                       |
| `sso_required`             | 403    | SSO is enforced for this workspace — use the SSO flow.                                                                                                                                                                                                              |
| `not_found`                | 404    | The requested resource does not exist.                                                                                                                                                                                                                              |
| `not_fitted`               | 404    | The requested computed resource can't be produced yet — e.g. a pattern's hazard model needs more history. `details.reason` explains why (`insufficient_events`, `no_timestamps`). Never a fabricated result — see [Abstention below](#abstention-is-not-a-failure). |
| `conflict`                 | 409    | The operation would create a duplicate (e.g. a service account with the same name).                                                                                                                                                                                 |
| `abstained`                | 409    | A classification step (extraction, root-cause, criticality) had no confident answer and abstained instead of guessing. The item is routed to `/review-queue`. Some endpoints instead surface this as a `status: "abstained"` field on a `200` response — see below. |
| `config_invalid`           | 422    | A config version body (`POST /configs/{kind}/versions`) failed schema or semantic validation. `details` maps field paths to messages, same shape as `validation_error`.                                                                                             |
| `rate_limited`             | 429    | Too many requests from this IP. See the `Retry-After` header.                                                                                                                                                                                                       |
| `internal_error`           | 500    | Unexpected server error.                                                                                                                                                                                                                                            |

## Abstention is not a failure

Causeloop's engine never guesses to fill in a gap. Several codes above, plus two non-error response shapes, are the mechanism:

* **Classification** (extraction, root-cause, criticality) returns `abstained` — either as the `409` error code above when the operation can't proceed at all, or as a `status: "abstained"` field in an otherwise-`200` response (e.g. `GET /issues/{id}/root-cause`) alongside a pointer into `/review-queue`. A human resolution appends an event and unblocks dependent computation; it is never silently skipped.
* **Hazard models** (`GET /patterns/{id}/hazard` and everything derived from it — forecasts, predictions, financials) return `404 not_fitted` with a `reason` when there isn't enough signal to fit. The frontend renders the reason, never a fake curve.
* **Reports**: every numeral in a generated narrative must resolve to the run's facts JSON. A mismatch fails the run outright with `report_facts_mismatch` rather than shipping a wrong number.
* **Replay**: `POST /engine/runs/{id}/replay` never errors on divergence. It always returns `200` with `{identical: bool, diff_summary}` — `identical: false` is a real (alarmed) outcome, not an exception.
* **Ingest**: an engine-eligible issue with no `narrative` is accepted (`202`) with `engine_status: "blocked_no_narrative"` in the ack's `engine_status_summary`, not rejected and not silently clustered without evidence.

## Validation errors

When request validation fails (status `400`, code `validation_error`), the `details` object maps field names to error messages:

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "trace_id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "details": {
      "title": "field required",
      "severity": "value is not a valid enumeration member"
    }
  }
}
```

The field name in `details` is the dot-path to the invalid field in the request body (e.g. `connector.config.api_key`).

## Handling errors

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function apiCall(url, options = {}) {
    const resp = await fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    if (resp.ok) return resp.json();

    const body = await resp.json().catch(() => ({}));
    const err = body.error ?? {};

    switch (err.code) {
      case 'unauthorized':
        // Token expired — refresh and retry
        await refreshToken();
        return apiCall(url, options);

      case 'validation_error':
        console.error('Validation failed:', err.details);
        throw new Error(`Validation error: ${JSON.stringify(err.details)}`);

      case 'rate_limited':
        // Handled separately in the retry wrapper — see Rate Limits docs
        throw Object.assign(new Error('Rate limited'), { retryAfter: resp.headers.get('Retry-After') });

      case 'not_found':
        throw Object.assign(new Error(`Not found: ${err.message}`), { status: 404 });

      default:
        console.error(`API error [${err.trace_id}]:`, err.message);
        throw new Error(`API error (${err.code}): ${err.message}`);
    }
  }
  ```

  ```python Python theme={null}
  import requests

  class CauseloopAPIError(Exception):
      def __init__(self, code, message, trace_id, details=None, status=None):
          super().__init__(message)
          self.code = code
          self.trace_id = trace_id
          self.details = details or {}
          self.status = status

  def api_call(method: str, url: str, **kwargs) -> dict:
      resp = requests.request(method, url, **kwargs)

      if resp.ok:
          return resp.json()

      try:
          err = resp.json().get('error', {})
      except Exception:
          err = {}

      code = err.get('code', 'unknown')
      message = err.get('message', resp.reason)
      trace_id = err.get('trace_id', '')
      details = err.get('details', {})

      raise CauseloopAPIError(code, message, trace_id, details, resp.status_code)

  # Usage
  try:
      issue = api_call(
          'GET',
          f'https://api.causeloop.ai/v1/issues/{issue_id}',
          headers={'Authorization': f'Bearer {access_token}'},
      )
  except CauseloopAPIError as e:
      if e.code == 'not_found':
          print(f'Issue {issue_id} not found')
      elif e.code == 'validation_error':
          print('Field errors:', e.details)
      else:
          print(f'Error [{e.trace_id}]: {e.code} — {e}')
  ```
</CodeGroup>

## 5xx errors and retries

`500 Internal Server Error` responses indicate an unexpected server-side failure. They are safe to retry with exponential backoff. Always include the `trace_id` from the error body if you report the issue to support.

<Tip>
  Pass the `trace_id` to Causeloop support when reporting unexpected errors — it ties directly to the server-side logs for that request.
</Tip>
