# API reference | Chersus

Documentation

# API reference

The complete POST /v1/run contract: headers, request and response fields, statuses, per-step billing, and the audit trace. Plus the service descriptor.

Last updated 2026-09-03

One endpoint runs everything. A second returns the contract for each service. This page specifies both, plus every field a response carries. For the walk-through, start with the [quickstart](/en/docs/quickstart/); for per-service examples, browse the [services directory](/en/services/).

## POST /v1/run

Runs a pipeline of one or more steps against an input. A single service call and a multi-step chain look the same on the wire.

```
POST /v1/run
Authorization: Bearer chrs_live_YOUR_KEY
Content-Type: application/json
Idempotency-Key: order-8812-check
```

| Header | Required | Purpose |
| --- | --- | --- |
| `Authorization` | yes | Bearer API key |
| `Content-Type` | yes | `application/json` |
| `Idempotency-Key` | no | Deduplicates retries for 24 hours. A repeat returns `status: "duplicate"` and the original `trace_id`, runs nothing, bills nothing. |

Every response also carries the rate-limit window in `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. Limits are per account; a chained run counts as one request. Maximum input is 100,000 characters per run, and the validator rejects larger inputs before anything runs. Region is an account setting, not a request field.

### Request body

```
{
  "request_id": "order-8812-check",
  "input": {
    "type": "text",
    "value": "Hi, I'm Jan de Vries, my IBAN is NL91ABNA0417164300, fix this or else."
  },
  "pipeline": [
    {
      "id": "shield",
      "service": "security.jailbreak.shield",
      "stop_if": { "verdict": "flag" }
    },
    {
      "id": "redact",
      "service": "governance.pii.redact",
      "params": { "entities": ["name", "iban"] }
    },
    { "id": "tox", "service": "security.toxicity.flag" },
    {
      "id": "intent",
      "service": "routing.intent.map",
      "params": { "catalog": "support-v3" }
    }
  ]
}
```

| Field | Required | Description |
| --- | --- | --- |
| `request_id` | no | Client correlation id, echoed back in the response |
| `input.type` | yes | `text` today. Reserved: `image`, `audio`, `video` |
| `input.value` | yes | Inline content. Media will use `input.uri` or `input.base64` instead |
| `pipeline` | yes | Ordered list of steps. One step is valid |
| `pipeline[].id` | yes | Unique step id, used as the key in `results` and `audit` |
| `pipeline[].service` | yes | Service name, e.g. `governance.pii.redact` |
| `pipeline[].params` | no | Service-specific, validated against the descriptor |
| `pipeline[].stop_if` | no | Classify steps only: `{ "verdict": "flag" }`, `{ "verdict": "pass" }`, `{ "score_gte": 0.9 }`, or `{ "score_lte": 0.1 }` |

Each step receives the payload as the previous step left it. When a `stop_if` fires, later steps do not run and do not bill. Halting on extract results needs [workflows](/en/docs/workflows/).

## GET /v1/services/:name

Returns the descriptor clients need to build valid chains.

```
{
  "name": "governance.pii.redact",
  "kind": "transform",
  "accepts": ["text"],
  "emits": "same",
  "billing_unit": "characters",
  "languages": "multilingual",
  "params": {
    "entities": { "type": "array", "default": ["name", "email", "id"] },
    "mask": { "type": "string", "default": "[REDACTED]" }
  },
  "model": { "id": "Roblox/roblox-pii-classifier-v2", "license": "Apache-2.0" }
}
```

| Field | Description |
| --- | --- |
| `kind` | `transform`, `classify`, or `extract`; fixes the result shape |
| `accepts` | Payload types the service accepts |
| `emits` | `"same"` (payload type unchanged) or a concrete type |
| `billing_unit` | What the service bills on; `characters` today, `pixels` and `seconds` reserved |
| `languages` | The languages the service handles: an array of ISO 639-1 codes, or `"any"` when it runs no language model, `"multilingual"` when its backend supports many without enumerating them, or `"unspecified"` when the backend publishes no claim |
| `params` | Typed parameters with defaults, validated per step |
| `labels` | Classify services only: every label the service can return |
| `output_schema` | Extract services only: the JSON schema `data` is validated against |
| `model` | Backend model id and license |

The validator uses `accepts` and `emits` to reject impossible chains before anything runs. There is no version in a service name: every call runs the latest version, and the audit trace records the version that ran.

## Response body

```
{
  "request_id": "order-8812-check",
  "trace_id": "chs_01J9X4Q7K2M8N3P5R6T7V8W9X0",
  "status": "completed",
  "halted_at": null,
  "output": {
    "type": "text",
    "value": "Hi, I'm [REDACTED], my IBAN is [REDACTED], fix this or else."
  },
  "results": {
    "shield": { "kind": "classify", "status": "ok", "verdict": "pass", "score": 0.03, "labels": [] },
    "redact": { "kind": "transform", "status": "ok", "changes": 2 },
    "tox": { "kind": "classify", "status": "ok", "verdict": "flag", "score": 0.84, "labels": ["threat"] },
    "intent": { "kind": "extract", "status": "ok", "data": { "intent": "billing.dispute", "confidence": 0.91 } }
  },
  "usage": {
    "billed": [
      { "unit": "characters", "quantity": 70, "steps": ["shield"] },
      { "unit": "characters", "quantity": 70, "steps": ["redact"] },
      { "unit": "characters", "quantity": 60, "steps": ["tox"] },
      { "unit": "characters", "quantity": 60, "steps": ["intent"] }
    ]
  },
  "audit": {
    "region": "eu-nl-1",
    "retention": "none",
    "received_at": "2026-09-03T09:14:22.118Z",
    "total_ms": 41,
    "steps": [
      {
        "id": "shield",
        "service": "security.jailbreak.shield",
        "version": 1,
        "model": "meta-llama/Prompt-Guard-86M",
        "status": "ok",
        "input_type": "text",
        "input_size": 70,
        "input_sha256": "9f2c...a1",
        "output_type": "text",
        "output_size": 70,
        "output_sha256": "9f2c...a1",
        "verdict": "pass",
        "duration_ms": 6
      },
      {
        "id": "redact",
        "service": "governance.pii.redact",
        "version": 2,
        "model": "Roblox/roblox-pii-classifier-v2",
        "status": "ok",
        "input_type": "text",
        "input_size": 70,
        "input_sha256": "9f2c...a1",
        "output_type": "text",
        "output_size": 60,
        "output_sha256": "4b71...c9",
        "changes": 2,
        "duration_ms": 11
      }
    ]
  }
}
```

| Field | Description |
| --- | --- |
| `request_id` | Echoed correlation id |
| `trace_id` | Unique run id |
| `status` | `completed`, `halted`, `failed`, or `duplicate` |
| `halted_at` | Step id where the run halted, else `null` |
| `output` | The final payload, `{ type, value }` |
| `results` | One result per executed step, keyed by step id |
| `usage.billed` | One line per executed step, with the characters that step processed |
| `audit` | The trace: region, retention, timing, and a record per step |

### Status values

| Status | Meaning |
| --- | --- |
| `completed` | Every step ran |
| `halted` | A `stop_if` fired (or a cap was hit). `halted_at` holds the step id; `output` is the payload at that point |
| `failed` | A step failed or validation rejected the pipeline. An `error` object replaces `output`; `results` still holds every step that completed before the failure |
| `duplicate` | The `Idempotency-Key` was seen in the last 24 hours. Only `trace_id` is returned |

### Result shapes by kind

Every result carries `kind` and `status`. The rest depends on the kind only.

| Kind | Fields |
| --- | --- |
| transform | `changes` (integer). `governance.pii.tokenize` adds `map` (token to original value) |
| classify | `verdict` (`pass`, `flag`, `uncertain`), `score` (0 to 1), `labels`. A `flag` always means the thing the service guards against was found; `score` is the probability of `flag`, so a `pass` has a low score |
| extract | `data` (JSON, validated against the service’s `output_schema`) |

### Billing fields

Each line in `usage.billed` carries `unit` (`characters`; `pixels` and `seconds` reserved), `quantity`, and the `steps` it covers. The short version of the rules:

 - Every executed step bills the characters it processes, at the tier rate. There is one rate table and no included steps.
 - A transform changes the payload, so a step after one bills what it was left rather than the original input.
 - A `stop_if` halt stops the count: steps that never ran never bill.
 - A run that fails part-way bills only the steps that completed. Chersus bills nothing for rejected requests or for its own failures.

The account’s total billed characters for the calendar month set the tier. Rates are marginal. The [pricing page](/en/pricing/) has the rate table and a calculator.

### Audit trace fields

Per run: `region`, `retention` (`"none"`), `received_at`, `total_ms`. Per step: the service and `version`, the `model`, input and output type and size, SHA-256 hashes of input and output, the verdict or change count, and `duration_ms`.

No payload content is ever included. Hashes are SHA-256 over the UTF-8 bytes of the payload exactly as received or produced, with no trimming or normalization. Recompute them and you can prove a given text went through a given step with a given result, without Chersus storing the text.

## Errors

Every error carries one envelope and one `code` to branch on, whether the request was rejected before it ran or a run failed part-way through. [Errors](/en/docs/errors/) documents the full set, the two levels they arrive at, and what each one bills.

Key handling is covered in [authentication](/en/docs/authentication/), and throttling in [rate limits](/en/docs/rate-limits/).