# Errors | Chersus

Documentation

# Errors

Every error Chersus returns: 401, 429, validation rejections, and 5xx. What each means, and what to do about it. One envelope, one code to branch on.

Last updated 2026-09-04

Chersus fails in two places, and the difference decides how you handle it. We reject a malformed request before anything runs, or a run fails part-way through a pipeline that had already started. The first is an HTTP error. The second is a `200` whose body says `status: "failed"`.

Branch on `error.code`, never on the message. Messages are for humans; we can reword them at any time. Codes are part of the contract.

## The envelope

Every error, at either level, carries the same shape. `details` appears when there is something specific to point at, such as the step that could not run:

```
{
  "error": {
    "code": "invalid_pipeline",
    "message": "Step 'tox' cannot accept the payload that step 'extract' emits.",
    "details": { "step": "tox" }
  }
}
```

## Rejected before anything runs

The request never reaches a model, so Chersus bills nothing.

| Code | HTTP | Meaning | What to do |
| --- | --- | --- | --- |
| `invalid_api_key` | `401` | The key is missing, malformed, or revoked | Check the `Authorization` header. A key that worked yesterday and fails today usually means someone rotated it: [authentication](/en/docs/authentication/) |
| `insufficient_quota` | `402` | The month’s character allowance is exhausted | Raise the tier or wait for the reset. Do not retry: the answer will not change until one of those happens. The [pricing page](/en/pricing/) has the rate table |
| `rate_limited` | `429` | Too many requests in the current window | Back off until `Retry-After` seconds have passed, then retry. See [rate limits](/en/docs/rate-limits/) |
| `invalid_request` | `400` | The body is malformed, or a required field is missing | Fix the request. `details.field` names the field when one is at fault |
| `unknown_service` | `400` | A step names a service that does not exist | Check the spelling against the [services directory](/en/services/). `details.step` names the step |
| `invalid_pipeline` | `400` | The pipeline cannot succeed as written: one step emits a type the next does not accept, or a `stop_if` sits on a step that returns no verdict | Reorder or drop the offending step. `details.step` names it |
| `input_too_large` | `413` | The input is over 100,000 characters | Split the input and run it in parts |

The four `400` and `413` codes all come from the validator, which runs before any compute. A rejected request costs nothing, so validating client-side is an optimization for latency, never for cost.

Retry only `429` and `5xx`. The rest describe something about the request that a retry will not change.

A server-side failure returns `5xx` with the same envelope. Chersus bills nothing, and a retry is safe: use an `Idempotency-Key` so a retry racing the original does not run twice.

Exhausted quota returns `402`, not the more common `429`, and the choice is deliberate. The two need different handling. A `429` means come back shortly and the same request will work; a `402` means nothing will change until the tier does. Folding them together leaves a client retrying a request that cannot succeed until somebody upgrades an account.

## Failed part-way through a run

The request was valid and the run started, so the response is a `200` carrying `status: "failed"`. An `error` object replaces `output`, and `results` still holds every step that completed before the failure.

```
{
  "trace_id": "chs_01J9X4Q7K2M8N3P5R6T7V8W9X0",
  "status": "failed",
  "error": {
    "code": "step_failed",
    "message": "The step could not process the payload it was given.",
    "details": { "step": "tox" }
  },
  "results": {
    "redact": { "kind": "transform", "status": "ok", "changes": 2 }
  },
  "usage": {
    "billed": [
      { "unit": "characters", "quantity": 61, "steps": ["redact"] }
    ]
  }
}
```

Only the completed steps bill. Read `results` to see how far the run got, and `usage.billed` to see what it cost. Same fact, one in steps and one in money.

A run-level failure is always `step_failed`, and `details.step` names the step that could not finish. We ship exactly one code here on purpose. Whichever service failed, your action is the same: read `results` for what completed and decide whether to rerun the rest.

A failure on Chersus’s own side never arrives this way. Those return `5xx`, bill nothing, and are safe to retry.

## Not errors, though they look like it

Two outcomes return `200` and are easy to mistake for failures.

A **halted** run is a success. A `stop_if` fired, or a workflow hit a cap, so the run stopped early on purpose. `status` is `"halted"`, `halted_at` names the last step that ran, and `output` is the payload as it stood. Steps that never ran never bill. This is the jailbreak shield doing its job, not an error.

A **duplicate** is also a success. A repeat with an `Idempotency-Key` from the last 24 hours returns `status: "duplicate"` with the original `trace_id`. Nothing ran and Chersus billed nothing, so keep the first response you got.

## What never bills

Worth stating in one place, because it spans all of the above.

 - A rejected request, at any of the codes in the first table.
 - A failure on Chersus’s side.
 - Any step that did not run, whether the run halted or failed before reaching it.

A run that fails part-way bills the steps that completed, and nothing else. The [chaining guide](/en/docs/chaining/) covers how that interacts with a pipeline.