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

# Errors

> Every error kind the API returns, what each one means, and what to do about it.

Every error carries a machine-readable `error_kind`. Branch on the kind, never on the status: several kinds share a status and want different remedies, and new kinds can be added within a status you already handle.

## The two envelopes

Parse defensively. Two shapes exist and they differ.

A **request-shape failure** (HTTP 422 from the validation layer) uses the `ValidationError` envelope, with an `errors` array of field locations so you can pinpoint the offending field:

```json theme={null}
{
  "error": "Validation error",
  "error_code": "validation_error",
  "status": 422,
  "retryable": false,
  "detail": "Validation error",
  "errors": [ { "loc": ["body", "observation", 0, "type"], "msg": "...", "type": "..." } ]
}
```

**Everything else** uses the `Error` envelope, where `detail` is either a string or a structured object carrying `error_kind`:

```json theme={null}
{
  "detail": {
    "error_kind": "turn_index_mismatch",
    "expected_turn_index": 4
  },
  "request_id": "b1c2d3e4...",
  "docs_url": "https://docs.ishlabs.io/api/errors#turn_index_mismatch"
}
```

Some kinds carry extra keys on `detail` alongside `error_kind`. Those are named per kind below. Whenever `detail` carries a registered `error_kind`, the body also carries `docs_url`: a direct link to that kind's entry on this page.

## Correlation

Every response carries an `X-Request-Id` header, and error bodies echo it as `request_id`. Quote it in any support request or bug report.

## Open union

`error_kind` is an open union. New kinds may be added inside a status you already handle, so treat an unrecognized kind as the generic case for its status rather than failing on it.

## 401 Unauthorized

<h3 id="invalid_api_key">
  invalid\_api\_key
</h3>

The bearer token is missing, malformed, unknown, or revoked. One kind covers all four deliberately: distinguishing them would confirm which guesses are real keys.

Check the `Authorization` header. If the key was revoked, mint a replacement in **Settings**, then **Developers**.

## 402 Payment Required

All three refuse the turn without applying it, and the session becomes terminal. Branch on the kind, because the first two share a terminal status of `balance_exhausted`.

<h3 id="credits_exhausted">
  credits\_exhausted
</h3>

No spendable credits are left. Top up.

<h3 id="plan_allowance_exhausted">
  plan\_allowance\_exhausted
</h3>

The plan's allowance for this period is spent. It refreshes on the date in the message, or top up to continue now.

<h3 id="spend_cap_reached">
  spend\_cap\_reached
</h3>

The workspace's own per-period spend cap was reached. Raise the cap, or wait for the period to roll over. The session's terminal status is `spend_cap_reached`.

## 403 Forbidden

<h3 id="insufficient_scope">
  insufficient\_scope
</h3>

The API key lacks the scope this operation requires. `detail.required_scope` names it. Mint a key with the scope, or ask for it if the key manages a registry rather than just running against it. The `:write` scopes on environments and tasks are never granted by default.

<h3 id="api_key_not_permitted">
  api\_key\_not\_permitted
</h3>

This operation does not admit API keys at all, and no scope grants it. Call it with an ish user access token.

<h3 id="person_out_of_scope">
  person\_out\_of\_scope
</h3>

A session can only be opened for a person the workspace owns or one published in the platform library. Real-person "me" profiles are out of scope by policy. Use an in-scope `person_id`.

## 404 Not Found

Each of these means the same thing: the record was not found, or it is not reachable with this credential. The two are deliberately indistinguishable, so a wrong id and someone else's id look identical. Check the id, and check that your token's workspace owns it.

<h3 id="workspace_not_found">
  workspace\_not\_found
</h3>

The `workspace_id` in the path.

<h3 id="session_not_found">
  session\_not\_found
</h3>

The `session_id` in the path.

<h3 id="person_not_found">
  person\_not\_found
</h3>

The `person_id` on a session create, or in the path on a person read. A person your workspace neither owns nor can reach in the platform library answers this rather than a `403`, so a guessed id cannot be used to test which ids are real.

<h3 id="environment_not_found">
  environment\_not\_found
</h3>

The `environment_id`, on a session create or an environment operation.

<h3 id="environment_version_not_found">
  environment\_version\_not\_found
</h3>

The `version_id`, or the `environment_version` label a session tried to pin.

<h3 id="task_not_found">
  task\_not\_found
</h3>

The `task.id` a session tried to bind, or the `task_id` in the path.

## 409 Conflict

<h3 id="idempotency_conflict">
  idempotency\_conflict
</h3>

The `Idempotency-Key` was reused with a different request body. A key maps to exactly one create request. Use a fresh key, or resend the identical body to replay the original.

<h3 id="session_not_open">
  session\_not\_open
</h3>

You posted a turn to a session that has already ended. The session is terminal; open a new one.

<h3 id="turn_index_mismatch">
  turn\_index\_mismatch
</h3>

The `turn_index` you sent does not match the session's current turn. `detail.expected_turn_index` carries the index the server expected. Resubmit at that index.

<h3 id="environment_name_taken">
  environment\_name\_taken
</h3>

An environment with this name already exists in the workspace. Names are unique per workspace, case-insensitive. With `get_or_create`, only a name held by a different `kind` conflicts.

<h3 id="environment_address_taken">
  environment\_address\_taken
</h3>

Another environment in the workspace already claims this address.

<h3 id="environment_archived">
  environment\_archived
</h3>

The environment resolves but is retired, so it cannot accept new sessions or new versions. Register a new environment, or describe one inline on the session.

<h3 id="environment_version_label_taken">
  environment\_version\_label\_taken
</h3>

The label collides with another live version of this environment. `detail.existing_version_id` carries the holder.

<h3 id="environment_version_archived">
  environment\_version\_archived
</h3>

The version is archived. An archived version cannot be pinned by a new session, and cannot become the default, since it would refuse the very sessions the pointer sent it.

<h3 id="environment_version_config_frozen">
  environment\_version\_config\_frozen
</h3>

`config` and `example_actions` are frozen once any session references this version, so a PATCH that carries either is refused. Metadata stays editable for the life of the row. Send only the fields you actually changed: a PATCH that echoes an unchanged `config` back is refused the same way as one that edits it.

<h3 id="environment_version_conflict">
  environment\_version\_conflict
</h3>

A concurrent write to this version won the race. Re-read and retry.

<h3 id="environment_version_default_conflict">
  environment\_version\_default\_conflict
</h3>

A concurrent `set-default` won the race. Re-read the environment and retry.

<h3 id="task_name_taken">
  task\_name\_taken
</h3>

A task with this name already exists in the workspace family. Names are unique per family, case-insensitive.

<h3 id="task_archived">
  task\_archived
</h3>

The task resolves but is retired. Register a new task, or pass the instructions inline on the session.

<h3 id="task_in_published_listing">
  task\_in\_published\_listing
</h3>

A published listing references this task, so it cannot be retired. `detail.listing_id` names the listing. Unpublish or edit the listing first.

## 422 Unprocessable Entity

Most 422s come from the request-validation layer and use the `ValidationError` envelope described above, with no `error_kind`. The two below are different: they are raised by the handler, carry the `Error` envelope, and name a kind. Both come from [people search](/api/guides/people-search), where the rule tree is something you author and we plan.

<h3 id="search_rule_invalid">
  search\_rule\_invalid
</h3>

The rule is malformed, or it names an attribute, source, or operator this API does not publish. `detail` names the offender: `source` and `key` for an unknown attribute, or the compiler's message for an operator that does not fit the attribute's type.

Check the rule against [`GET .../people/schema`](/api/reference/people/get-people-schema), which is the authoritative vocabulary: an attribute is filterable only if the schema gives it a `filter` block, and only with the operators that block lists.

<h3 id="search_rule_too_complex">
  search\_rule\_too\_complex
</h3>

The rule is well-formed but past a size cap: more than 2 levels of nested groups, more than 15 conditions in one group, or more than 25 in total. `detail` carries the offending count alongside the limit, so you do not have to bisect your own filter.

Combining values with `in` costs one condition instead of one per value, and is usually the whole fix.

## 429 Too Many Requests

Both are transient and both carry `Retry-After`. They reset on different clocks.

<h3 id="rate_limited">
  rate\_limited
</h3>

A rate limit upstream of your request was exceeded. On this surface that is the API key's own request rate limit, which resets within the minute; the same kind is used elsewhere on the platform when an upstream provider limits us, which is why the remedy is expressed as "retry later" rather than "slow down". Honor `Retry-After`, also echoed as `detail.retry_after_seconds`. Not applied to user access tokens.

<h3 id="session_quota_exceeded">
  session\_quota\_exceeded
</h3>

The API key's daily session-create quota is used up. It resets on a day boundary. Existing sessions are unaffected, so a loop already running keeps running; only new creates are refused. API keys only, not user tokens.

## 500 Internal Server Error

None of these is a state to handle. Quote `request_id` and the kind in a bug report.

<h3 id="session_actor_missing">
  session\_actor\_missing
</h3>

<h3 id="payer_unresolved">
  payer\_unresolved
</h3>

<h3 id="environment_unresolved">
  environment\_unresolved
</h3>

## 502 Bad Gateway

<h3 id="decision_invalid">
  decision\_invalid
</h3>

The participant produced an invalid choice twice, and no turn was recorded. Retriable: resubmit the **same** `turn_index` with a fresh frame.

## Related

<Columns cols={2}>
  <Card title="Run the session loop" icon="repeat" href="/api/guides/session-loop">
    Where each error lands in the loop, and how to recover without losing the turn.
  </Card>

  <Card title="Authentication" icon="key" href="/api/authentication">
    Scopes, rotation, and what a key can reach.
  </Card>
</Columns>
