API reference
The complete contract: POST /v1/run, policies, authentication, rate limits, errors, statuses, billing rules, and the audit trace, in one place.
One endpoint does the work, POST /v1/run. List the operations inline, or name a policy. Both answer in the same shape. The quickstart is the walk-through, and the contract is also an OpenAPI document.
Operations
Every operation has a full name, kind.subject.action. A request lists each kind under its own name, and inside that list an operation goes by its short name: pii.entities in detect, pii.tokenize in transform. Wherever one stands on its own (when, routing.by, blocked_by, failed_ops, the audit trace) it keeps its full name, such as detect.pii.entities.
| Kind | Returns | Never |
|---|---|---|
detect.* |
verdict (pass, flag), score (0 to 1), labels |
rewrites |
extract.* |
structured data: a value with confidence, or a JSON object |
rewrites |
transform.* |
rewritten text, plus changes |
reads without rewriting |
An operation is a string, or an object with options, such as { "op": "secrets.credentials", "block": true }. Both forms name the same operation.
| Option | Operations | Meaning |
|---|---|---|
min_score |
every detect | The score at or above which the detect flags. Without it, the model’s own verdict |
block |
every detect | When the detect flags, the decision is block and no text is handed back. The detect and extract results still come back |
schema |
fields.schema |
The fields to fill, by name and type |
types |
pii.redact, pii.tokenize |
The entity types to act on |
mask |
pii.redact, secrets.redact |
The placeholder, default [REDACTED] |
when |
every transform | The outcomes it waits for. See conditions and routing |
An option on the wrong operation is rejected with invalid_request. GET /v1/ops returns the catalogue with each operation’s options. The operations page lists all sixteen.
POST /v1/run
Operations on one piece of text, written inline or named as a policy.
| Header | Required | Purpose |
|---|---|---|
Authorization |
yes | Bearer chrs_live_YOUR_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. Chersus keeps no response, so keep the first one |
X-LLM-Key |
with relay, unless the model is provided |
Your credential for the relay’s provider. See your provider credential |
X-LLM-Key-<model name> |
when routing names a model | The credential for that model in routing.models, such as X-LLM-Key-eu |
X-Session-Key |
no | Any opaque id, such as one per conversation. Placeholders are then stable within that key only. See the token map |
Request body
{
"input": "Contact Jane Doe at jane.doe@example.com. Your service is garbage.",
"transform": ["pii.tokenize"],
"detect": ["toxicity.abuse", "secrets.credentials"],
"extract": ["language.code"]
}
| Field | Description |
|---|---|
input (required) |
The text, as a string. At most 100,000 characters |
policy |
A policy name (latest active version) or a pinned ref name@version, in place of inline operations |
conditions |
Up to 32 names your code sets to true or false, such as { "high_security": true }. Lowercase letters, digits, - and _, no dot |
detect |
Detects, in parallel on the original input. A detect with block: true decides the run |
extract |
Extracts, in parallel with the detects |
transform |
An ordered list. Each receives the output of the previous one. A transform with when runs only when one of its outcomes holds |
tokens |
A token map from an earlier response. The response returns it merged with the new tokens, so one map can serve a whole conversation. See the token map |
relay |
Sends what the last transform handed back to a model and returns the answer as output. provider is openai, anthropic, mistral, azure, bedrock or vertex with your key in X-LLM-Key, or chersus for a provided model. model names the model (azure takes endpoint and deployment instead), detect runs checks on the answer (before rehydration, so on placeholders), routing picks the model. See relay and choosing a model for each provider’s fields |
stream |
Relay only. true, or Accept: text/event-stream, streams server-sent events: route, delta, detect, blocked, done |
At least one of detect, extract, transform or relay is required, or a policy. A policy next to any of them is rejected with invalid_request. An operation in another kind’s list is rejected with invalid_op_kind, and a full name inside a list with invalid_op.
Conditions and routing
A transform waits for an outcome with when, written source:outcome. A relay routes on one value with routing: by names the source, and each case is one of its outcomes. The workflows guide explains how they combine.
| Source | Outcomes | Example |
|---|---|---|
| a detect | flagged, clean |
detect.pii.entities:flagged |
extract.language.code |
An ISO 639-1 code | extract.language.code:de |
extract.content.topics |
Each of 36 topics, several at once | extract.content.topics:travel |
a name from conditions |
true, false |
high_security:true |
| Field | Description |
|---|---|
routing.by |
The one source that decides. Topics can hold several outcomes at once, so they work in when but not here |
routing.models |
Named models, each with provider and model. Each needs its own X-LLM-Key-<name> |
routing.cases |
Outcome to model name, or null for no model: the run goes to the output. An outcome without a case goes to the relay’s own model |
The error codes are unknown_condition, unknown_outcome, unknown_model, invalid_condition_name and llm_key_missing.
Response body
{
"trace_id": "chs_01J9X4Q7K2M8N3P5R6T7V8W9X0",
"status": "completed",
"decision": "allow",
"output": "Contact <PERSON_eb7805d77a13> at <EMAIL_33b537052b41>. Your service is garbage.",
"transform": [{ "op": "pii.tokenize", "changes": 2 }],
"detect": {
"toxicity.abuse": { "verdict": "flag", "score": 0.84 }
},
"tokens": { "<PERSON_eb7805d77a13>": "Jane Doe", "<EMAIL_33b537052b41>": "jane.doe@example.com" },
"usage": {
"billed": { "unit": "characters", "input": 66, "output": 79, "quantity": 145 }
},
"audit": { "...": "see the audit trace below" }
}
| Field | Description |
|---|---|
trace_id |
Unique run id, also in the audit trace |
status |
completed, failed or duplicate. See statuses |
policy |
With a policy: the pinned ref that ran |
decision |
allow or block. block only when a detect with block: true flagged, on the input or on the answer |
blocked_by |
On block: the checks that flagged, with op, score and labels |
failed_ops |
On failed: the operations that did not run |
output |
The last transform’s text, or with relay the model’s answer. null when the run had neither |
relay |
provider, model, duration_ms, and detect with one result per answer check. Absent when routing skipped the model |
route |
With routing: by, outcome, the routed model’s name (null for the relay’s own model), provider and model (for azure, the deployment). All null but by and outcome when it skipped the model |
detect, extract |
One result per operation, keyed by short name. Absent when the run had none |
transform |
One record per transform, in order: op, changes, skipped: true when its when did not hold |
tokens |
The full token map. Present only when a tokenize ran |
usage.provided_model |
Provided-model relay only: input_tokens, output_tokens, provider_cost, fee (5% of the provider cost), currency |
usage.billed |
unit, input (characters sent), output (characters handed back, 0 without a transform) and quantity, their sum, which bills |
audit |
The trace: hashes, never content |
On block, output, transform and tokens are omitted, detect and extract stay, and only the input bills. An input check that blocks never calls the model, so relay is absent. A block by an answer check withholds the answer: relay is present, output is not.
Policies
A policy is a workflow saved under a name and versioned. The policies guide walks through the object.
{
"name": "customer-support",
"extends": "template:external-llm",
"detect": [{ "op": "prompt.injection", "block": true }, "pii.entities"],
"transform": [{ "op": "pii.tokenize", "when": ["detect.pii.entities:flagged", "high_security:true"] }],
"conditions": { "high_security": false },
"relay": { "provider": "openai", "model": "gpt-4.1-mini" },
"tokens": { "style": "angle" }
}
| Field | Description |
|---|---|
name |
Posting a name that exists creates the next version |
extends |
One parent, one level: a template (template:external-llm) or a policy of yours (support-base@2). Frozen into the version at creation. A parent that extends something is rejected with extends_depth |
detect, extract, transform, relay |
Exactly as in run. At least one is required |
conditions |
Defaults for the conditions the policy uses. A request overrides them per name |
tokens.style |
angle (default, <PERSON_eb7805d77a13>), bracket ([PERSON_eb7805d77a13]) or curly ({{PERSON_eb7805d77a13}}) |
Merging with a parent: detect and extract are the union, with the child’s options winning. transform is the parent’s list, then the child’s. relay and tokens are the child’s when set. conditions is the union, with the child’s default winning.
Routes are in the endpoint list. Posting returns name, version, ref (customer-support@3), status and created_at. Nothing is edited in place: customer-support resolves to the latest active version at request time, customer-support@3 is pinned. A retired version is refused with policy_retired but stays readable for audit traces. The only active version cannot be retired. Reads return the resolved object plus the raw definition. Templates (external-llm, internal-llm, strict-pii, support-chat, rag-context, tool-call) are owned by Chersus, and an update never reaches your policy until you publish a new version.
Running a policy
POST /v1/run with a policy in place of the operations. Same headers and response. The key needs the apply scope and the policy in its allowlist, or the run is refused with policy_not_allowed. See the policies guide.
{
"policy": "customer-support@3",
"input": "My name is John Smith and my account number is 123456.",
"conditions": { "high_security": true }
}
Fields: policy and input (required), conditions, tokens and stream.
Authentication
Keys start with chrs_live_ or chrs_test_, and you see the full key once, at creation. Test keys run the same models, bill nothing, and are rate-limited low. Keep keys out of client code. After a rotation the old key stops working at once, so deploy the new one first.
| Key property | Meaning |
|---|---|
scopes |
run for inline operations, apply for runs that name a policy, policies:read, policies:write |
policies |
An allowlist, wildcard versions allowed (customer-support@*). Others fail with policy_not_allowed |
environment |
production, staging or development. Appears in the audit trace |
Your provider credential
A relay to your own provider account takes its credential in X-LLM-Key. Provided models need none. It is used once and never stored. The audit trace records its SHA-256 fingerprint. A routed model gets its own header, X-LLM-Key- plus its name in routing.models. Send every routed model’s key up front, since the case is only known after the detects and extracts ran. Choosing a model lists what each provider takes.
Rate limits
Limits scale with volume and apply per account, per minute. A run is one request, however many operations it holds. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp).
| Limit | Value |
|---|---|
| Window exhausted | 429 with Retry-After in seconds. Never billed |
| Input per run | 100,000 characters, else input_too_large |
| Test keys | Rate-limited low |
| Idempotency | An Idempotency-Key and its trace_id are kept 24 hours, no content |
| Provider limits | Still apply on the model leg. A provider 429 is provider_error |
Retry only 429 and 5xx, with a token bucket rather than a tight loop. Region is an account setting, not a request field. Ask about reserved capacity for dedicated throughput.
Errors
Branch on error.code, never on the message. A rejected request is an HTTP error and bills nothing.
{
"error": {
"code": "invalid_op_kind",
"message": "'pii.redact' is a transform; pass it in transform, not in detect.",
"details": { "op": "transform.pii.redact" }
}
}
Rejected before anything runs
| Code (HTTP) | Meaning |
|---|---|
unauthorized (401) |
The key is missing, malformed or revoked, often rotated |
insufficient_quota (402) |
The month’s allowance is exhausted. Do not retry before the reset |
rate_limited (429) |
Too many requests. Wait Retry-After seconds |
invalid_request (400) |
Malformed body or missing field. details.field |
invalid_op (400) |
The operation does not exist, or a full name sits in a list. details.op |
invalid_op_kind (400) |
An operation in another kind’s list. details.op |
field_reserved (400) |
A reserved field was sent. details.field |
input_too_large (413) |
Input over 100,000 characters |
policy_not_found (404) |
No policy with that ref |
policy_retired (400) |
The version is retired |
policy_not_allowed (403) |
The key’s allowlist excludes this policy |
extends_depth (400) |
The parent already extends something |
llm_key_missing (400) |
No X-LLM-Key, or a routed model’s X-LLM-Key-<model name>. details.provider, details.model |
relay_field_missing (400) |
The relay lacks a field its provider needs, such as endpoint for azure. details.field |
unknown_condition (400) |
A when or routing.by names a detect or extract the run lacks, or a condition with no request value and no policy default. details.condition |
unknown_outcome (400) |
An outcome its source cannot take. details.outcome |
unknown_model (400) |
A routing case names a model not in routing.models. details.model |
invalid_condition_name (400) |
A condition name outside a-z, 0-9, - and _. details.condition |
Quota is a 402, not a 429, because a 429 clears in seconds and a 402 only at the reset. A 5xx carries the same envelope and is safe to retry with an Idempotency-Key.
Provider side and after the run started
| Code (HTTP) | Meaning |
|---|---|
provider_auth_failed (401) |
The provider rejected your credential. Your Chersus key was fine |
provider_timeout (504) |
The provider did not answer in time. Retry with an Idempotency-Key |
provider_error (502) |
The provider returned an error. details.provider_status and details.provider_code carry it |
Through the gateway, these arrive in the provider’s own error format. A run that fails after it started is a 200 with status: "failed", failed_ops and a null output. A block and a duplicate also return 200 and are not errors: a block is a check doing its job, and a duplicate is a success.
Statuses
| Status | Meaning |
|---|---|
completed |
Every operation ran. decision is independent of status |
failed |
An operation failed on the Chersus side. Nothing handed back, nothing billed. failed_ops lists which |
duplicate |
The Idempotency-Key was seen in the last 24 hours. Only trace_id and status are returned |
Billing
The month’s characters, in and out together, set the rate: the first 1M are free, then €0.06 per 1K, falling 20% for every doubling of volume. The pricing page has the full rule.
- A completed run: input characters plus the characters handed back in
output. Without transforms or relay,outputis null and only the input bills. decision: blockon the input: input characters, inline or with apolicy.- A completed relay: input characters plus the rehydrated answer handed back. Relay adds nothing.
- A provided model adds its provider cost plus 5% as a separate line.
- A relay that an answer check blocks: the input, and for a provided model the tokens the provider spent.
- A relay that fails on the provider side: nothing.
- A transform skipped by its
when: nothing. A run that routing sends to no model bills as a run without relay. - A failed run: nothing.
- A rejected request (4xx): nothing.
- A failure on Chersus’s side (5xx): nothing.
- A duplicate: nothing.
- Detect and extract: nothing, ever.
- Test keys: nothing.
Audit trace
Every run response carries an audit trace. Hashes, never content.
"audit": {
"trace_id": "chs_01J9X5A1...",
"region": "eu-nl-1",
"retention": "none",
"policy": "customer-support@3",
"decision": "allow",
"total_ms": 31,
"input_sha256": "9f2c1e...a1",
"output_sha256": "6b7d44...0f",
"ops": [
{
"op": "transform.pii.tokenize",
"version": 2,
"model": "pii-entities-2",
"duration_ms": 26,
"changes": 2,
"input_sha256": "9f2c1e...a1",
"output_sha256": "6b7d44...0f"
}
]
}
| Level | Fields |
|---|---|
| Run | trace_id, region, retention, environment, the decision, the policy when one ran, the route when routing ran, total_ms, SHA-256 of input and output |
Relay (a relay block) |
provider, model, key_fingerprint (SHA-256 of the LLM key), prompt_sha256, response_sha256 and duration_ms. Never the key or the prompt |
| Operation | The operation and its version, the model, duration_ms, the verdict, value or change count, and its own hashes |
Hashes are SHA-256 over the UTF-8 bytes exactly as received or produced. They prove that a given text went through a given policy version with a given result, using only the hash of text you hold. Chersus retains for invoicing only trace_id, key, policy ref, decision, character counts and timestamp. Not the hashes, not the text, not the map.
Endpoint list
POST /v1/run
GET /v1/policies list
POST /v1/policies create version 1, or the next version
GET /v1/policies/{name} latest active version, resolved
GET /v1/policies/{name}@{version} a pinned version, resolved
GET /v1/policies/{name}/versions every version and its status
DELETE /v1/policies/{name}@{version} retire a version
GET /v1/templates the templates Chersus ships
GET /v1/templates/{name}
GET /v1/ops the operation catalogue
gw.chersus.com/openai/v1/*
gw.chersus.com/anthropic/*
Ten routes and a gateway. The gateway is the same relay at a base URL, for existing OpenAI and Anthropic SDKs, with bodies, streaming and provider errors in the provider’s own format. The gateway guide has SDK examples. Its headers:
| Gateway header | Required | Purpose |
|---|---|---|
| The SDK’s own key header | yes | Your LLM key, unchanged |
X-Chersus-Key |
yes | Your Chersus API key |
X-Chersus-Policy |
no | Overrides the gateway default policy set in the dashboard |
X-Session-Key |
no | Placeholders stable within that key only |