# API reference

Zaun scan API 0.2.0. Requests go to `https://api.zaun.ai`. The machine-readable contract is [openapi.yaml](/openapi.yaml), also served as [openapi.json](/openapi.json).

Assess prompts and documents synchronously, then use the verdict, findings,
coverage, and stages to decide what your application does next. Scans send
text to a contextual classifier: the cascade, or the direct classifier, as the
project's classifier mode selects. Confidence describes support for the reported verdict
and is not a calibrated probability; unknown means evidence is insufficient.

Results and usage belong to your key's project. Recommendations are advisory:
even block is a successful HTTP 200 assessment, and your application enforces
the decision. Prompt and document scans, result read/delete, and usage queries
are implemented with PostgreSQL authentication and atomic usage persistence.
Token-bucket admission and trusted client IP recording are implemented. Scan
content cannot select its organization or project. Assessments use a model classifier with automatic fallback; results never
identify the models, corpus or routing behind a decision. Document extraction is configured separately.
Operator management is implemented with independent adminBearer authentication.
Batch scanning remains reserved. Assessed generation through POST /v1/responses uses a
configured model catalog.

## Scans

Prompt assessments and multipart document scans; batch is reserved.

### POST /v1/scan/prompt

Scan messages and tool calls. Operation `scanPrompt`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

**Every message in the messages array is scanned**, including system, developer,
user, assistant and tool messages, except messages the request's session has already
scanned. **Include only the messages you want scanned**.

A request can carry input text, output text, function tool calls, or any mix of
the three. Tool calls are inspected, never run. For an assistant message that only
calls tools, set `content` to `null` and put the calls in `tool_calls`. Each call's
`function.arguments` is a JSON object serialized as a string.

**Sessions.** Pass a `session_uuid` to scan a conversation across several requests.
The API remembers which messages it has already scanned for that session and
assesses only the new ones, so you can send the whole history, a recent window, or
just the latest messages. The classifier still sees the recent conversation for
context. Each call is billed once: the tokens it assesses for the first time at the
scan rate (`usage.input_tokens`) and the session's earlier tokens the classifier reads
back at a cached rate of a tenth of it (`usage.cached_input_tokens`). The response's
`session` object reports how the request was matched against the session's history.

**Verdict.** The response is one combined verdict based on your project's controls.
The API does not block anything itself. Your application applies the returned
action. When a Redact control matches, the response includes `redacted_prompt`.

**Images.** User messages may carry up to 4 PNG or JPEG images per request, sent as
`image_url` data URLs. The API reads their text with OCR and scans it where the
image appeared. Classifiers see it as `[image]` followed by the recognized text.
Each image adds USD 0.0075 to the scan rate for its tokens, charged when the scan
completes. If a Redact control matches text inside an image, that text is redacted
in `redacted_prompt`. The image itself is not changed.

**Limits.** The request body may be up to 20 MiB. Long inputs are scanned in
overlapping windows. The API key needs the `scan:write` scope.

**Keeping the result.** The response's `scan_uuid` identifies the stored result. Use
it with `GET /v1/result/{scan_uuid}`, `DELETE /v1/result/{scan_uuid}` and
`POST /v1/scan/feedback`. The same value is returned in the `X-Scan-Request-Id`
header.

**Retries.** An `Idempotency-Key` header makes a retried request replay the original
result instead of scanning and billing again. Keys that begin with `ingest:` are
reserved for agent telemetry and return 422.

Request body (required): `application/json`, [PromptInput](#promptinput)

Responses:

| Status | Description |
| --- | --- |
| `200` | Successful compact assessment. Technical failures use the error envelope. [CompactScanResult](#compactscanresult) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/scan/feedback

Submit scan feedback. Operation `submitScanFeedback`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Records your review of a retained scan in the authenticated project. The API key
needs the `result:read` scope.

Feedback is a record only. It does not change classification, controls or training.

Submitting again from the same API key updates that key's existing review for the
scan rather than adding a second one.

A `scan_uuid` that is unknown, expired, deleted or owned by another project returns 404.

Request body (required): `application/json`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) |  |
| `reason` | yes | string; one of `incorrect_detection`, `wrong_technique` |  |
| `notes` | no | string or null | Optional review notes. Do not include secrets or personal data. |

Responses:

| Status | Description |
| --- | --- |
| `200` | Review saved. object |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/scan/document

Assess a document. Operation `scanDocument`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Uploads one document and scans its text. The API key needs the `scan:write` scope.

The response's `scan_uuid` identifies the result.

**Synchronous and asynchronous.** By default the request waits for the scan to finish.
When the service is configured with a document job store (`SCAN_DOCUMENT_JOBS_BUCKET`),
you can send `Prefer: respond-async` instead. The API then returns 202 once the input
is stored encrypted and the job is durably committed to PostgreSQL. The 202 response
carries `scan_uuid`, a `Location` header and `Retry-After`. Poll
`GET /v1/result/{scan_uuid}` with a key that has `result:read`. Without a job store,
asynchronous requests return 503.

**How queued jobs run.** A queued job frees its HTTP admission slot. Dedicated
document-worker processes claim jobs with renewable leases, retry transient failures
and fence off stale workers. Each attempt has a 90-second budget. A job allows three
failed attempts and expires after one hour. A job deferred for capacity stays pending
until that deadline.

**Size and formats.** The file may be at most 10,485,760 bytes (10 MiB) inside an 11 MiB
multipart body. Supported formats are pdf, csv, doc, docx, xls, xlsx, html, txt and md.
The format is detected from the bytes, not the file name.

**How text is extracted.** Only extracted text reaches the classifier. Plain text
formats (txt, md, csv, html) are scored exactly as uploaded and never transcribed.
Office formats (doc, docx, xls, xlsx) go through a bounded native parser first, and to
the document model only if the parser cannot read them. A PDF is read from its own
text layer when that layer covers every page, reported as `extraction_source`
`pdf_text_layer` with `coverage` `complete`. A scanned or partly scanned PDF is
transcribed by the document model with OCR, inside the stage's time budget.

**Reading the result.** Text pulled from binary formats may be imperfect, so a benign
classification stays inconclusive (`unknown`) rather than clean. Check `coverage`,
`stages`, `per_chunk` and `extraction_evidence` before relying on a result. PDF
evidence keeps text-free page geometry and OCR confidence, never the recognized words.

**Errors.** A transcription the model refuses, times out on, or returns empty is
reported as a stage outcome, not an HTTP error: status 200 with `status` `failed`,
`error.stage` `document_model` and `review_recommended` `true`. HTTP 4xx is reserved
for requests rejected before any provider work: unsupported bytes, blank text, and
size or format limits. A 503 with code `capacity` means no execution slot was free.
Retry the request.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Prefer` | header | no | string | Use respond-async to acknowledge a claimed scan before extraction completes. |

Request body (required): `multipart/form-data`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `sample` | no | boolean | Controls whether Zaun shows you this request's text. Zaun keeps a redacted copy of every request unless the project sets retain_activity to false. With sample=false the copy is kept but never returned by GET /v1/sessions or shown in the console. It is still used for session context and campaign detection, and classification and billing are unchanged. |
| `file` | yes | string (binary) | Raw file bytes, never a URL. |
| `session_uuid` | no | string (uuid) | Groups the document scan with other scans of the same session. Version 8 UUIDs are reserved for the sessions Zaun derives from agent telemetry and return 422. |
| `metadata` | no | [Metadata](#metadata) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Successful compact assessment. Technical failures use the error envelope. [CompactScanResult](#compactscanresult) |
| `202` | Document processing is in progress. Poll the owned result URL. [DocumentJob](#documentjob) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/batch/scan-prompts

Assess an ordered batch. Operation `batchScanPrompts`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Reserved for a future build. Every call currently returns 501 `operation_not_implemented`.

When it ships, the contract is this. A batch holds up to 25 items, each with a unique
`item_id`, all scanned under one configuration version. Results come back in input
order inside an HTTP 200 batch response. Each item carries its own result or its own
error, including deadline failures. Items draw on their class rate budgets. Batches
also have their own four-slot concurrency budget and a 60-second deadline. The API
key needs the `scan:write` scope, and the request needs an `Idempotency-Key` header.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | yes | string | Bound to project, operation and canonical body. Scan retries replay the original result. Keys beginning with `zaun:`, in any letter case, are reserved for Zaun and refused with invalid_request. |

Request body (required): `application/json`, [BatchInput](#batchinput)

Responses:

| Status | Description |
| --- | --- |
| `200` | Ordered per-item results, including errors. [BatchResult](#batchresult) |
| `501` | This build does not implement this operation. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Responses

Assessed, stateless generation with OpenAI SDK compatible Responses and streaming.

### POST /v1/responses

Create a response. Operation `createResponse`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Generates a model response for a conversation, with Zaun's input scan built in. The
API key needs the `run:write` scope.

**Stateless.** Send the full conversation in `input` on every request. `store=true`
and `previous_response_id` are rejected, and responses are not retained for later
retrieval.

**Built-in scanning.** You do not need a separate `POST /v1/scan/prompt` call or the
`scan:write` scope. Zaun freezes the input, assesses the whole conversation, persists
that assessment, enforces the project's controls at their pinned policy version, and
then calls the selected model once. The input assessment is returned in `zaun.scan`
alongside the response. Generated function calls are scanned before release and
reported in `zaun.output_scan`. Plain assistant text is not output-scanned
automatically.

The `zaun` extension is part of the same Responses object. The OpenAI Python SDK
exposes it through `response.model_extra`. It is not copied into standard `metadata`.

**Order of operations.** Input regex controls run first. A regex Block stops the
request before any classifier or model call. Any other regex match waits for input
classification before generation starts. With no regex matches, generation and input
classification run in parallel. All output waits until the input permission is
persisted. Generated tool calls also wait for output assessment; plain text does not.
A classifier that completes but abstains defaults to `no_action`. If assessment
fails, output is withheld.

**Models.** Use an ID returned by `GET /v1/models`. Zaun resolves the configured
route internally, so transport identifiers are not accepted as model IDs. An unknown
alias returns 422 before inference. A server with no configured generation model
returns 501.

**Billing and output limits.** Generation is billed at the token rates returned by
`GET /v1/models`, separately from the input and output scans. Output is capped at the
model's own `zaun.max_output_tokens`, at most 128000. When `zaun.sampling` is
`exclusive`, send `temperature` or `top_p` but not both. When it is `disabled`, send
neither.

**Reasoning effort.** `reasoning.effort` asks the model for more or less work on the
whole response, including any private thinking. Each model lists the levels it accepts
and its default in `zaun.effort` from `GET /v1/models`; a level outside that list, any
other `reasoning` field, and an effort on a model whose `zaun.effort` is null all return
422 `invalid_request` with `param` `reasoning.effort` before any paid call. Thinking is
never returned, and `usage.output_tokens` includes it at every level. A non-streaming
response must finish within 180 seconds and a stream must release its first text within
180 seconds, thinking included, so send `stream: true` at `high` and above.

**Input items.** Items may be messages with text or inline image content,
`function_call` items and `function_call_output` items. Inline PNG and JPEG images
are accepted on models that support vision and return 422 `unsupported_content`
elsewhere. Remote image URLs, files, audio, built-in tools, developer messages and
system messages that appear after the conversation has started are rejected. The
first non-system item must be a user message. The last item must be a user message
or a function call output.

**Streaming.** OpenAI SDK clients can call `responses.create`, including with
`stream=true`. The stream sends typed events: `response.created`,
`response.in_progress`, then, once input is permitted, text as
`response.output_item.added`, `response.content_part.added` and
`response.output_text.delta`, each followed by its matching `.done` event. Tool
calls stay buffered until assessed and arrive as `function_call` output items with
`response.function_call_arguments.done`. The stream ends with `response.completed`,
or `response.incomplete` for `max_output_tokens` or `content_filter`, carrying the
full response. A failure after streaming has started ends with an `error` event.

**Blocked and unavailable.** A blocked request returns 403 with the decision in
`zaun`. The `error.code` is `input_block` when a regex control matched before any
classifier or model call, or `policy_block` when the assessed verdict met a control
set to Block. The code `forbidden` is reserved for credential and scope failures. An
assessment the gate cannot trust returns 503 `assessment_unavailable` with
`Retry-After`. Nothing was generated, so retry the request.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | no | string | Optional project-scoped generation fence. Reuse with the same body returns 409 without another generation because response output is not retained for replay. A changed body returns idempotency_conflict. Keys beginning with `zaun:`, in any letter case, are reserved for Zaun and refused with invalid_request. |

Request body (required): `application/json`, [ResponsesRequest](#responsesrequest)

Responses:

| Status | Description |
| --- | --- |
| `200` | Generated output with its unchanged assessment and enforcement decision. [ResponsesResponse](#responsesresponse) string |
| `403` | The request was blocked by the project's controls, never by a credential failure. error.code is input_block when a regex control stopped the request before any classifier or model call (zaun.input_checks names the controls), or policy_block when the assessed verdict met a behavioral control set to Block (zaun.scan carries the assessment). Both set zaun.enforcement to blocked. Credential and scope failures use the Error envelope with code forbidden and no zaun body. object |
| `501` | This build does not implement this operation. [ErrorEnvelope](#errorenvelope) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Results

Project-scoped retained results and usage aggregates.

### GET /v1/sessions/{session_uuid}

Retrieve retained redacted session activity. Operation `getSessionActivity`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Returns the retained, redacted activity for one session in the authenticated project.
The API key needs the `result:read` scope.

**Correlating a session.** Send the same `session_uuid` on prompt scans, or
`zaun.scan.session_uuid` on completions, to tie input and output assessments
together. The value does not change classification. Events keep message roles and
order, and the two halves of a completion share an `operation_id`.

**Ordering and paging.** Events are ordered by request start time and then event ID,
which is not always the order they ran in. Pages are served from a stable snapshot
taken at insertion time. Start pagination again to pick up events that arrived late.

**What is retained.** Activity is captured asynchronously, redacted, and kept for 30
days. A complete history for a session is not guaranteed. Conversation context that
repeats across requests is stored with each request. Text can be available before
its embedding is ready.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `session_uuid` | path | yes | string (uuid) |  |
| `cursor` | query | no | string | Opaque next_cursor from the preceding page. |
| `limit` | query | no | integer |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | A page of captured activity, possibly empty. object |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/sessions/{session_uuid}/timeline

List a session's scans as metadata. Operation `getSessionTimeline`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>) or `delegatedToken` (JWT).

Lists the scans the authenticated project ran with this `session_uuid` in request order,
as the same metadata rows `GET /v1/violations` returns. Every verdict is included, and
so are scans of the published test string. No message text is returned; read one
result's redacted text at `GET /v1/violations/{scan_uuid}/evidence`. The API key needs
`violation:read` or `result:read`. A delegated user token needs `zaun:violations:read`
and names the project in `project_id`.

`client_session_id` is the agent's own session id when a client hook or telemetry
source supplied one. A session the project never used returns an empty list. Pass
`next_cursor` back as `cursor` for the next page. Only retained results are listed.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `session_uuid` | path | yes | string (uuid) |  |
| `project_id` | query | no | string (uuid) | The project to read. Required with a delegated user token, which names an organization; the user needs a current role on the project, or the call returns 403. A project key reads its own project, so omit it or name that project; any other returns 403. |
| `limit` | query | no | integer |  |
| `cursor` | query | no | string | Opaque next_cursor from the preceding page. |

Responses:

| Status | Description |
| --- | --- |
| `200` | One page of the session's scans in request order (`created_at`), possibly empty. [SessionTimeline](#sessiontimeline) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/result/{scan_uuid}

Read a retained project result. Operation `getResult`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Reads a retained assessment in the key's project. The API key needs the `result:read`
scope.

Any active key in the same project with this scope can read the result, including
after the original key is rotated.

**Status codes.** A UUID that is missing, deleted or owned by another project returns
404. An expired result returns 410. A document scan that is still running returns 202
with `Retry-After`. A document scan whose execution lease expired returns 200 with a
failed `DocumentJob`.

**Reading the result.** Read `status`, `verdict`, `coverage` and `stages` together.
Between them they tell a completed assessment apart from one with incomplete evidence.

Polling works across API replicas and does not need a ready corpus.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `scan_uuid` | path | yes | string (uuid) | The scan_uuid from a scan response, or the request_uuid of a completion. |

Responses:

| Status | Description |
| --- | --- |
| `200` | Persisted assessment, including inconclusive or failed stage outcomes. [ScanResult](#scanresult) or [DocumentJob](#documentjob) |
| `202` | Document processing is in progress. Poll the owned result URL. [DocumentJob](#documentjob) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### DELETE /v1/result/{scan_uuid}

Delete a project result. Operation `deleteResult`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Deletes a retained assessment in the key's project. The API key needs the
`result:delete` scope.

Deleting the same result again in that project returns 204. A UUID that is missing
or owned by another project returns 404.

Deletion clears the result content and its idempotent replay link. A tombstone
covers the retry window so late work cannot recreate the result. Historical usage and
audit records remain. A provider request that is already running cannot be undone.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `scan_uuid` | path | yes | string (uuid) | The scan_uuid from a scan response, or the request_uuid of a completion. |

Responses:

| Status | Description |
| --- | --- |
| `204` | Deleted, or already deleted in this project. |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/violations

List the project's violations. Operation `listViolations`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>) or `delegatedToken` (JWT).

Lists the authenticated project's summarized results, newest first. The API key needs
`violation:read` or `result:read`. Rows carry metadata only, never message text: read
one result's redacted text at `GET /v1/violations/{scan_uuid}/evidence` with
`result:read`.

**Delegated users.** When an agent calls through Zaun's MCP server, it presents a
delegated user token instead of a key. The token names an organization, so it names
the project in `project_id`, and every call checks that the user is still active and
still holds a role on that project. Every call that passes is recorded with the user,
the agent's client, the project, the route and the request id before any data is read.
The token needs `zaun:violations:read`. A missing
`project_id` returns 422; a project outside the token's organization, or one the user
has no role on, returns 403. A project key may omit `project_id`, or name its own
project; another project returns 403.

**Rows.** Each row is flat: the result's identifiers, when it ran, the detection
verdict, the strongest action the API reported (`block`, then `redact`, then `alert`,
then `no_action`), the severity class when the result carries one, the detected
labels, the confidence, how the content reached Zaun (`channel`), its source and
reported user when known, a console link and whether its evidence can be read.

**Interval.** Give `since` (inclusive) and `until` (exclusive) as RFC 3339 timestamps or
as a duration before now (`30m`, `24h`, `7d`), spanning at most 90 days. `from` and `to`
are accepted as before and take RFC 3339 only. `until` defaults to now and `since` to
seven days before it. The interval and the order apply to `completed_at`. Only retained
results are listed: an expired or deleted result leaves the feed.

**Filters.** `verdict`, `action`, `severity` and `channel` take one value or several
separated by commas, matching any. Without `verdict`, only violations are listed.
`technique` names one detected label. `source` matches a source family such as
`claude_code` or a source id. `principal` takes a user's email, id or name, or a hash
as rows report it. `session_uuid` and `import_id` match exactly. `test` decides
whether scans of the published test string are listed: `exclude` (the default),
`include` or `only`.

**Paging.** Pass the previous page's `next_cursor` as `cursor` with the same interval
and filters. The cursor is opaque and must fall inside the interval.

**Errors.** Unknown parameters, invalid dates or durations, `from` with `since` or `to`
with `until`, unknown filter values, limits outside 1 to 100, malformed or
out-of-interval cursors, and empty, reversed or oversized intervals return 422.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `project_id` | query | no | string (uuid) | The project to read. Required with a delegated user token, which names an organization; the user needs a current role on the project, or the call returns 403. A project key reads its own project, so omit it or name that project; any other returns 403. |
| `since` | query | no | string | Inclusive start, as RFC 3339 or a duration before now (30m, 24h, 7d). Defaults to seven days before until. |
| `until` | query | no | string | Exclusive end, as RFC 3339 or a duration before now. Defaults to now. |
| `from` | query | no | string (date-time) | Inclusive start as RFC 3339; the earlier name for since. Do not send both. |
| `to` | query | no | string (date-time) | Exclusive end as RFC 3339; the earlier name for until. Do not send both. |
| `limit` | query | no | integer |  |
| `cursor` | query | no | string | Opaque next_cursor from the preceding page. |
| `verdict` | query | no | string | Detection verdicts, comma-separated: violation, unknown, benign. Defaults to violation. |
| `action` | query | no | string | Strongest recorded actions, comma-separated: no_action, alert, redact, block. |
| `severity` | query | no | string | Severity classes, comma-separated: threat, risk_escalate, risk_monitor, benign. Rows without a severity never match. |
| `technique` | query | no | string | One detected label the row must carry, such as Credential Access. |
| `channel` | query | no | string | How the content reached Zaun, comma-separated: api, hook, ingest, import, playground. |
| `source` | query | no | string | A source family such as claude_code, or a source id. |
| `principal` | query | no | string | A user. Type an email address, id or name as the agent reports it and Zaun hashes it with your organization's key to find the telemetry results that user produced; case and surrounding space do not matter. A hash exactly as rows report it also matches, as does an identifier a direct API caller sent as `metadata.principal_hash`. The hash in rows is made with a key only Zaun holds, derived per organization, so it cannot be computed from an email outside Zaun. If an email cannot be resolved the response is 503 `principal_lookup_unavailable`, which retrying does not fix; a hash still works. |
| `session_uuid` | query | no | string (uuid) | Only results of this session. |
| `import_id` | query | no | string | Only results of this batch import. |
| `test` | query | no | string; one of `exclude`, `include`, `only` | Whether scans of the published test string are listed. |

Responses:

| Status | Description |
| --- | --- |
| `200` | One page of violations in the interval, possibly empty. [ViolationList](#violationlist) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/violations/summary

Count the project's violations by one dimension. Operation `summarizeViolations`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>) or `delegatedToken` (JWT).

Counts the authenticated project's summarized results that match the same interval and
filters as `GET /v1/violations`, grouped by `group_by`. The API key needs
`violation:read` or `result:read`. A delegated user token needs `zaun:violations:read`
and names the project in `project_id`, as on `GET /v1/violations`.

- `category`: the project control a label belongs to, such as `credential_access`;
  a Self-Harm finding counts under `self_harm`, not `risky`. Labels no control names
  are left out of this grouping.
- `technique`: each detected label. A result with two labels counts in both groups.
- `source`: the source family; `principal`: the reported user's hash; `channel`;
  `action`; `day`: the UTC day of `completed_at` as `YYYY-MM-DD`.

`total` counts each matching result once, including results with no value for the
dimension. Groups come largest first, at most `limit` of them; days come newest first,
so a limit keeps the most recent days. A null `key` collects results without the value,
such as direct API scans grouped by source. Unknown parameters, a missing or unknown
`group_by`, invalid filters and limits outside 1 to 100 return 422.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `project_id` | query | no | string (uuid) | The project to read. Required with a delegated user token, which names an organization; the user needs a current role on the project, or the call returns 403. A project key reads its own project, so omit it or name that project; any other returns 403. |
| `group_by` | query | yes | string; one of `category`, `technique`, `source`, `principal`, `day`, `action`, `channel` |  |
| `limit` | query | no | integer | The most groups returned. |
| `since` | query | no | string | Inclusive start, as RFC 3339 or a duration before now (30m, 24h, 7d). Defaults to seven days before until. |
| `until` | query | no | string | Exclusive end, as RFC 3339 or a duration before now. Defaults to now. |
| `from` | query | no | string (date-time) | Inclusive start as RFC 3339; the earlier name for since. Do not send both. |
| `to` | query | no | string (date-time) | Exclusive end as RFC 3339; the earlier name for until. Do not send both. |
| `verdict` | query | no | string | Detection verdicts, comma-separated: violation, unknown, benign. Defaults to violation. |
| `action` | query | no | string | Strongest recorded actions, comma-separated: no_action, alert, redact, block. |
| `severity` | query | no | string | Severity classes, comma-separated: threat, risk_escalate, risk_monitor, benign. Rows without a severity never match. |
| `technique` | query | no | string | One detected label the row must carry, such as Credential Access. |
| `channel` | query | no | string | How the content reached Zaun, comma-separated: api, hook, ingest, import, playground. |
| `source` | query | no | string | A source family such as claude_code, or a source id. |
| `principal` | query | no | string | A user. Type an email address, id or name as the agent reports it and Zaun hashes it with your organization's key to find the telemetry results that user produced; case and surrounding space do not matter. A hash exactly as rows report it also matches, as does an identifier a direct API caller sent as `metadata.principal_hash`. The hash in rows is made with a key only Zaun holds, derived per organization, so it cannot be computed from an email outside Zaun. If an email cannot be resolved the response is 503 `principal_lookup_unavailable`, which retrying does not fix; a hash still works. |
| `session_uuid` | query | no | string (uuid) | Only results of this session. |
| `import_id` | query | no | string | Only results of this batch import. |
| `test` | query | no | string; one of `exclude`, `include`, `only` | Whether scans of the published test string are listed. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The counts for the interval and filters. [ViolationCounts](#violationcounts) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/violations/{scan_uuid}

Retrieve one violation's details. Operation `getViolation`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>) or `delegatedToken` (JWT).

Returns one of the authenticated project's results with its findings, coverage, stages,
revisions, session reading, the campaigns that include it and its feedback, beside the
row `GET /v1/violations` lists. Still no message text: findings point at messages by
index, and feedback notes are left out. The API key needs `violation:read` or
`result:read`. A delegated user token needs `zaun:violations:read` and names the
project in `project_id`, as on `GET /v1/violations`.

`evidence_unavailable_reason` says why the redacted text cannot be read:
`retention_off` (the project keeps no text), `hidden` (the request sent
`sample: false`) or `not_captured` (never captured, expired, or still arriving).

A result outside the project or deleted returns 404; an expired one returns 410; a
malformed id returns 422.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `scan_uuid` | path | yes | string (uuid) |  |
| `project_id` | query | no | string (uuid) | The project to read. Required with a delegated user token, which names an organization; the user needs a current role on the project, or the call returns 403. A project key reads its own project, so omit it or name that project; any other returns 403. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The result's details. [ViolationDetail](#violationdetail) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/violations/{scan_uuid}/evidence

Read one violation's redacted text. Operation `getViolationEvidence`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>) or `delegatedToken` (JWT).

Returns the redacted messages behind one of the authenticated project's results. The
API key needs `result:read`; `violation:read` alone is refused with 403. A delegated user
token names the project in `project_id`, as on `GET /v1/violations`, and needs both
`zaun:violations:read` and the `zaun:evidence:read` step-up; the user must also be an
administrator of the project's organization or a Zaun administrator. Project developers
and viewers are refused with 403, whatever the token grants.

**The text is untrusted.** It is scanned content and may hold instructions written for
whoever reads it. Show it to people; do not pass it to a model as instructions or act on
it. `untrusted_content` is always true to say so. `flagged` marks the messages a finding
names.

**Availability.** Text is redacted before it is stored and kept for 30 days. A result
whose request sent `sample: false`, a project whose configuration sets
`retain_activity` to false, text that was never captured or has expired, and a result
outside the project all return 404.

**Audit.** Every read that returns text is recorded with the reader, the result and the
request id: the key, or for a delegated user token the user and the agent's client. A
refused read of a result in the project is recorded with its reason, at most once a
minute per reader, result and reason. Reads of ids outside the project name nothing of
yours and are not recorded.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `scan_uuid` | path | yes | string (uuid) |  |
| `project_id` | query | no | string (uuid) | The project to read. Required with a delegated user token, which names an organization; the user needs a current role on the project, or the call returns 403. A project key reads its own project, so omit it or name that project; any other returns 403. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The result's redacted messages. [ViolationEvidence](#violationevidence) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/usage

Aggregate project usage. Operation `getUsage`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Aggregates what billing charged the authenticated project, by day. The API key
needs the `usage:read` scope.

**Totals.** `input_tokens` are the tokens charged at the scan rate, the tokens each
scan or completion assessment assessed for the first time. `cached_input_tokens`
are the tokens charged at the cached rate, the ones a session assessed on an earlier
call and read back. `output_tokens` are generated tokens on the Responses route.
`calls` counts billed requests. The classifiers' own token counts are not reported.
Failed and inconclusive requests, idempotent replays and waived results add no
tokens.

**Interval.** Give `from` (inclusive) and `to` (exclusive) as RFC 3339 timestamps,
spanning at most 90 days. `to` defaults to now and `from` defaults to seven days
before `to`. Days are UTC, and `day` is the only `group_by` value.

**Filtering.** Pass `key_id` to limit results to one key in the same project. A
missing or other-project key ID returns 404.

**Errors.** Invalid dates, unknown parameters, and empty, reversed or oversized
intervals return 422.

Rotating keys and deleting results do not remove historical usage. A valid interval
with no usage returns an empty `usage` array and zero totals.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `from` | query | no | string (date-time) |  |
| `to` | query | no | string (date-time) |  |
| `group_by` | query | no | string; one of `day` | Usage is grouped by UTC day. Model and stage breakdowns are not available. |
| `key_id` | query | no | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Daily usage totals in the requested interval. object |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Administration

Organizations, projects, immutable policy versions and one-time keys, protected by separate organization admin keys or a platform operator credential.

### POST /v1/claims/{claim_id}/redeem

Redeem a key claim from a command line. Operation `redeemKeyClaim`. No authentication.

Collects the key a claim stands for, from a command line on the user's machine. A
claim is what an agent receives when it asks for a key: the secret never passes
through the agent. No authentication is required, because the claim id alone grants
nothing.

The command line generates a random `verifier` (at least 32 bytes, base64url) and
keeps it. The first call binds the claim to that verifier and returns 202 with an
`approval_url` and a `user_code`; print both. The user opens the URL, signed in to the
console as the person the claim was issued to, and types the code to approve. The
console never displays the code, so only the person at this terminal can approve it.
The command line keeps calling with the same verifier
(honor a few seconds between calls); after approval the next call returns 201 with
the key's metadata and its `secret`, once, with `Cache-Control: no-store`.

Calls are limited per client address and per claim, and calls that name no claim are
limited separately per address. Past any limit the API returns 429 `rate_limited` with
a `Retry-After` header; wait that long before calling again. Polling every five seconds
stays well within the limits. The first call records its address and user agent, and
the console shows them to the user beside the approval.

A claim expires ten minutes after it is issued and can be redeemed once. Unknown and
expired claims return 404. A different verifier returns 403. A claim that was
already redeemed, here or on the console's claim page, returns 409
`key_already_created` and never returns the secret again. If the user who received
the claim has since lost admin access to the organization, the key is not issued. A
claim stored with any scopes other than exactly `["scan:write"]`, which creation no
longer accepts, returns 422 `invalid_request` and no key is issued.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `claim_id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `verifier` | yes | string |  |

Responses:

| Status | Description |
| --- | --- |
| `201` | Approved; the key was issued now. The secret appears once; Cache-Control is no-store. [KeyMetadata](#keymetadata) and object |
| `202` | The claim is bound to this verifier and waits for the user's approval. Cache-Control is no-store, because the body carries the user code. [KeyClaimPending](#keyclaimpending) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/organizations

List organizations. Operation `listOrganizations`. Authentication: `Authorization: Bearer` with `adminBearer`.

Lists organizations with their metadata and enabled state.

Use it to find the permanent ID of an organization you want to set up or maintain.
Customer scan keys cannot list organizations.

Responses:

| Status | Description |
| --- | --- |
| `200` | Organizations and enabled state. array of [Organization](#organization) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/organizations

Create an organization. Operation `createOrganization`. Authentication: `Authorization: Bearer` with `adminBearer`.

Creates an organization and returns its permanent ID. Customer scan keys cannot call
this.

The request carries a name and an optional description. Its projects scan as soon as
they hold a key.

Request body (required): `application/json`, [CreateMetadata](#createmetadata)

Responses:

| Status | Description |
| --- | --- |
| `201` | Organization metadata; identity is immutable. [Organization](#organization) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/organizations/{org_id}

Read organization metadata. Operation `getOrganization`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reads one organization's metadata and enabled state by its permanent ID.

Renaming an organization does not change its ID or the scope of its projects and
keys. Authenticate with an organization admin key minted in Organization settings,
or with the platform operator credential.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `org_id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Organization metadata; identity is immutable. [Organization](#organization) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### PATCH /v1/admin/organizations/{org_id}

Update organization metadata or enabled state. Operation `updateOrganization`. Authentication: `Authorization: Bearer` with `adminBearer`.

Updates an organization's name, description or enabled state. Its ID does not change.

Disabling an organization rejects new scans and new key issuance. Keys that are
otherwise still active keep read and delete access to saved results until they
expire. Only the platform operator credential can make this change.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `org_id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, [UpdateMetadata](#updatemetadata)

Responses:

| Status | Description |
| --- | --- |
| `200` | Organization metadata; identity is immutable. [Organization](#organization) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/organizations/{org_id}/projects

List projects in an organization. Operation `listProjects`. Authentication: `Authorization: Bearer` with `adminBearer`.

Lists the projects in the selected organization.

Use it to find the permanent ID of a project before managing its policy or keys.
An organization admin key can list projects only in its own organization. Customer
scan keys are bound to one project and cannot list this collection.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `org_id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Projects in the selected organization. array of [Project](#project) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/organizations/{org_id}/projects

Create a project and its initial policy version. Operation `createProject`. Authentication: `Authorization: Bearer` with `adminBearer`.

Creates a project in the selected organization, together with its first policy
version.

Keys, results, usage and idempotent retries are all scoped to the project. Projects
never share customer results.

Fields in the `config` object merge recursively over the `SCAN_POLICY_JSON`
defaults. Arrays replace the default instead of merging.

An organization admin key can create projects only in its own organization.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `org_id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, [CreateProject](#createproject)

Responses:

| Status | Description |
| --- | --- |
| `201` | Project metadata; organization ownership is immutable. [Project](#project) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}

Read project metadata and active configuration version. Operation `getProject`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reads a project's metadata, owning organization, enabled state and active
configuration version.

Address the project by its permanent ID. Authenticate with the platform operator
credential, or with an organization admin key when the project belongs to that
organization. Customer requests cannot switch projects through request content.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Project metadata; organization ownership is immutable. [Project](#project) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### PATCH /v1/admin/projects/{id}

Update project metadata or enabled state. Operation `updateProject`. Authentication: `Authorization: Bearer` with `adminBearer`.

Updates a project's name, description or enabled state. Its ID and owning
organization do not change.

Disabling a project rejects new scans and new key issuance. Keys that are otherwise
still active keep read and delete access to retained results. Moving a project to
another organization is not supported.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, [UpdateMetadata](#updatemetadata)

Responses:

| Status | Description |
| --- | --- |
| `200` | Project metadata; organization ownership is immutable. [Project](#project) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/config

Read published configuration. Operation `getProjectConfig`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reads the project's published policy and its version number.

The policy includes application context, rules and limits. Read the version here
before publishing an update, and send it back in `If-Match`. Each scan uses the
configuration version that was current when it was admitted, and records that
version in its result.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Published configuration and immutable version; ETag is the version for If-Match. object |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### PUT /v1/admin/projects/{id}/config

Publish a new immutable configuration version. Operation `publishProjectConfig`. Authentication: `Authorization: Bearer` with `adminBearer`.

Validates and publishes a new policy version. Each version is immutable.

Send the current version number as a quoted numeric `If-Match` header. A missing or
malformed header returns 422 `invalid_request`. A version that is no longer current
returns 412 `precondition_failed`.

PUT replaces the whole policy. Requests admitted after the publish use the new
policy. Work already admitted keeps the version it was pinned to.

Rules must use known labels and confidence thresholds between zero and one.
Application context is limited to 4,000 characters.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `If-Match` | header | yes | string |  |

Request body (required): `application/json`, [Policy](#policy)

Responses:

| Status | Description |
| --- | --- |
| `200` | Published configuration and immutable version; ETag is the version for If-Match. object |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/ingest

Read whether the project accepts telemetry. Operation `getProjectIngest`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reads the project's telemetry kill switch. Ingestion is on until it is first turned
off.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | The switch. [IngestSettings](#ingestsettings) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### PUT /v1/admin/projects/{id}/ingest

Turn the project's telemetry on or off. Operation `setProjectIngest`. Authentication: `Authorization: Bearer` with `adminBearer`.

Turns the project's telemetry on or off. While it is off, every ingest key of the
project is refused with 401 before its body is read, and the refusal is counted on the
key's source as `quota`. Scans are unaffected. The change is audited.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `enabled` | yes | boolean |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | The switch after the change. [IngestSettings](#ingestsettings) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/keys

List key metadata and status. Operation `listKeys`. Authentication: `Authorization: Bearer` with `adminBearer`.

Lists the keys in the selected project, including revoked ones.

Each entry has the key ID, display prefix, scopes, expiry and revocation metadata.
Responses never include plaintext secrets or hashes. Identify a key for rotation or
revocation by its ID, not its display prefix.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Project key metadata, including revoked keys. array of [KeyMetadata](#keymetadata) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/projects/{id}/keys

Issue a project key with one-time secret delivery or as a claim. Operation `createKey`. Authentication: `Authorization: Bearer` with `adminBearer`.

Issues a new project-bound API key. The secret is returned exactly once.

The request needs an `Idempotency-Key` header. The response is sent with
`Cache-Control: no-store`. Zaun stores only the SHA-256 hash of the secret plus
lifecycle metadata.

**Retries.** Retrying with the same body after the key is committed returns 409
`key_already_created` with the key's metadata but never the secret. Retrying with a
different body returns 409 `idempotency_conflict`. If you lost the response that
carried the secret, revoke that key ID and create a replacement with a new
idempotency key. Idempotency claims last for the lifetime of the issued key, even
after it is revoked or expires.

Customer scan keys cannot issue keys. A disabled organization or project rejects new
keys with 409 `disabled`.

**Ingest keys.** A key with `scopes: ["ingest:write"]` and a `source_id` from the same
project is an ingest key: its secret starts `zaun_ingest_`, it can only send telemetry
to that source, and it cannot scan, read results or administer. Both fields are required
together. Another project's source returns 404; a disabled source returns 409 `disabled`.

**Claims.** Zaun's MCP server, acting for a signed-in user, must send
`delivery: "claim"` and receives a claim instead of a secret, so the key never enters
an agent's context. The claim is bound to that user, the organization and the agent
that asked, and expires after ten minutes. The user opens its `url` in the console to
reveal the key once, or approves a command line that redeems it with
`POST /v1/claims/{claim_id}/redeem`. Retrying with the same idempotency key and body
returns the same claim with 200. A user can create five claims an hour; the sixth
returns 429. Organization admin keys and the operator credential cannot request a
claim, and the MCP server cannot request a secret; both return 403. A claim mints a key that
can run scans and nothing else: `scopes` must be exactly `["scan:write"]`, and any other
list, an omitted one included, returns 422 `invalid_request` naming `scopes`. A claim cannot
carry an ingest key: `source_id` with `delivery: "claim"` returns 422, as does
`daily_cap_micros`, naming that field. When more than one rule is broken, the first of
`source_id`, `scopes` and `daily_cap_micros` is the one named.

**Daily cap.** Give a key that spends with no person in the loop, such as a fleet's
Claude Code hook key, a `daily_cap_micros`. Once its settled spend for the UTC day
reaches the cap, its calls return 402 `key_daily_cap` until midnight UTC.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `Idempotency-Key` | header | yes | string | Bound to project, operation and canonical body. Scan retries replay the original result. Keys beginning with `zaun:`, in any letter case, are reserved for Zaun and refused with invalid_request. |

Request body (required): `application/json`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | yes | string |  |
| `expires_at` | no | string (date-time) or null |  |
| `scopes` | no | [Scopes](#scopes) |  |
| `source_id` | no | string (uuid) or null | Binds an ingest key to a telemetry source of this project. Required with, and only with, scopes ["ingest:write"]. |
| `delivery` | no | string; one of `secret`, `claim` | `secret` returns the plaintext once. `claim` returns a claim instead and is required of, and only available to, Zaun's MCP server acting for a user. |
| `daily_cap_micros` | no | [DailyCapMicros](#dailycapmicros) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | A retry of a claim request; the same claim is returned. [KeyClaimCreated](#keyclaimcreated) |
| `201` | With delivery secret: the plaintext appears once and Cache-Control is no-store. With delivery claim: a claim and no secret. [KeyMetadata](#keymetadata) and object or [KeyClaimCreated](#keyclaimcreated) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### DELETE /v1/admin/projects/{id}/keys/{key_id}

Revoke a key by immutable ID. Operation `revokeKey`. Authentication: `Authorization: Bearer` with `adminBearer`.

Revokes a key by ID within its project.

Revoking the same key again returns 204. A key ID that is missing or belongs to
another project returns 404.

Once the revocation commits, every replica rejects the key on later authorization
checks. Work already admitted can finish. The key's metadata, the project's results
and its usage history remain available to replacement keys with the right scopes.

Zaun's MCP server cannot call this route for a user. A request with a delegated token
returns 403 `forbidden` and no key is revoked; revoke keys in the console.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `key_id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `204` | Revoked; metadata and usage history remain. |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/results/{scan_uuid}

Read a stored result with its diagnostics. Operation `getProjectResult`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reads one of the project's results as stored, diagnostic signals included.

For an operator's parity or latency read of a Zaun-owned project. The body is the stored
result, not the public projection: it keeps every stage's latency and the assessment's
internal signals. It holds no prompt text. Missing and expired results fail as the
customer's result read does. Only the platform operator credential reads it: organization admin keys
and delegated user tokens are refused.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `scan_uuid` | path | yes | string (uuid) | The scan_uuid of a result the project owns. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The stored result. [StoredResult](#storedresult) |
| `202` | A document still being read; poll again. [StoredResult](#storedresult) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/projects/{id}/detection-tests

Run a detection sample through the project. Operation `runDetectionTest`. Authentication: `Authorization: Bearer` with `adminBearer`.

Runs one published detection sample through the project's own scan path and returns
the scan result, so detection can be proven without handling an API key. The sample
is named by id from `GET /v1/detection-samples`; a test never carries its own text.

The scan is admitted and billed like any prompt scan and is stored as a result of the
project. It runs under the project's **Detection tests** key, which Zaun creates on
first use with a secret nobody receives. Revoking that key in the console turns
detection tests off for the project (403). Running the same sample again within the
same ten-minute window returns the first result without another charge, so a retry
is safe; a run in a later window scans again.

`matched` is true when the result has the sample's expected verdict and an action at
least as strong as the expected one (Block, then Redact, then Alert, then No action).
Organization admin keys and Zaun's MCP server can call it for their own projects; the
MCP server needs only the `zaun:read` scope, although the first test creates the
internal key.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, object

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `sample_id` | yes | string | An id from GET /v1/detection-samples. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The sample, the stored scan result and whether it met the expectation. object |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Service

Public liveness and reference documentation.

### GET /healthz

Process liveness. Operation `health`. No authentication.

Reports whether the API process is running. No authentication is required.

A successful response means only that the process is up. It does not mean your
organization is ready to scan. The response carries no tenant, credential or
dependency details.

Responses:

| Status | Description |
| --- | --- |
| `200` | The process is alive. object |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/openapi.json

Download the OpenAPI contract. Operation `openapi`. No authentication.

Downloads this API's OpenAPI contract as JSON. No authentication is required.

The contract is OpenAPI 3.1 and describes this exact build: request shapes, result
fields, examples and error envelopes. The browsable reference at /v1/docs is built
from the same contract.

Responses:

| Status | Description |
| --- | --- |
| `200` | Checked-in YAML converted to JSON at compile time. object |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/docs

Browse the API reference. Operation `docs`. No authentication.

Redirects to the browsable API reference. No authentication is required.

The reference lives at https://console.zaun.ai/docs.

Responses:

| Status | Description |
| --- | --- |
| `308` | Permanent redirect to the console documentation. |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/detection-samples

List the detection samples. Operation `listDetectionSamples`. No authentication.

Lists the detection samples: Zaun-written scan requests, one or more per category
plus benign controls and the published test string, each with the outcome expected
under the self-serve default controls. No authentication is required.

Send a sample's `messages` to `POST /v1/scan/prompt` with your own key to see your
project's result. The test string `ZAUN-TEST-VIOLATION-7f3a9c` is always reported as
a blocked input check named `zaun_test`, on prompts and inside tool arguments alike,
so hooks, gateways and alerts can be proven without sending a real attack. It is not a
configurable control.

Responses:

| Status | Description |
| --- | --- |
| `200` | The checked-in sample catalog. [DetectionSampleCatalog](#detectionsamplecatalog) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Models

Authenticated discovery of configured generation models.

### GET /v1/models

List configured generation models. Operation `listModels`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Lists the model aliases that `POST /v1/responses` accepts. The API key needs the
`run:write` scope and the project must be active and ready.

The call runs no inference and consumes no scan usage. An empty catalog returns 200
with an empty `data` array.

The list describes server configuration. It is not a live check of provider access or
health. Model routing is internal and never returned.

Responses:

| Status | Description |
| --- | --- |
| `200` | Configured generation aliases in OpenAI list format; created is zero when no creation timestamp is tracked. [ModelList](#modellist) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/providers/{provider}/models

Legacy model discovery. Operation `listProviderModels`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Older model discovery, kept for compatibility. Use `GET /v1/models` instead.

The API key needs the `run:write` scope and the project must be active and ready. The
only accepted provider name is `bedrock`. Any other name returns 404 after
authentication. With no configured models it returns 200 with an empty `data` array.
No provider calls are made.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `provider` | path | yes | string |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | Configured generation aliases in OpenAI list format; created is zero when no creation timestamp is tracked. [ModelList](#modellist) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Telemetry

OpenTelemetry (OTLP/HTTP) receiver for agent telemetry, authenticated by ingest keys.

### POST /v1/ingest/otlp/v1/logs

Receive OpenTelemetry logs from an agent. Operation `exportLogs`. Authentication: `Authorization: Bearer` with `ingestKey` (zaun_ingest_<64 hex>) or `ingestKeyHeader`.

Authenticate with an ingest key in `x-zaun-ingest-key` (recommended: exporters parse
header lists as `key=value` pairs, and this form needs no space) or
`Authorization: Bearer`. The key's source and project decide where the telemetry goes;
no attribute, header or payload field can choose another. The key is checked before the
body is read.

Send `application/x-protobuf` or `application/json`, uncompressed or with
`Content-Encoding: gzip`. Limits: 4 MiB as sent; 32 MiB decompressed, and at most ten
times the compressed size above 4 MiB; 10,000 records; 1,000 resource or scope groups;
200,000 attribute values and 64 MiB of decoded text, counting the copies of span
attributes in span events; attribute values nested at most 8 levels; 64 KiB per
attribute key. Over any of those limits the request is refused whole with 413 and
counted on the source. A text attribute value or log body over 64 KiB does not refuse
the request: the start and the end of the value are kept (32 KiB each, cut on UTF-8
character boundaries, with a note between them of how many bytes were left out) and the
event is marked as partially captured, so a long pasted prompt does not drop the rest
of its batch. Zaun assesses the text it kept. Over the telemetry allowances it is 429 with `Retry-After`; when
the server's decoders are busy it is 503 with `Retry-After`. Events more than five minutes ahead of the
server clock or older than the project's result retention are dropped individually and
reported as partial success.

Point an exporter's `OTEL_EXPORTER_OTLP_ENDPOINT` at `/v1/ingest/otlp` and it appends
`/v1/logs`. Log records become events named by the producer (for example
`claude_code.user_prompt`); unknown names are kept.

**Request shape** (`application/json`; protobuf carries the same message). Fields not
shown are accepted and ignored; full field notes are in `OtlpLogsExportRequest`.

```json
{
  "resourceLogs": [{
    "resource": {"attributes": [
      {"key": "service.name", "value": {"stringValue": "claude-code"}}
    ]},
    "scopeLogs": [{
      "scope": {"name": "com.anthropic.claude_code.events", "version": "2.1.177"},
      "logRecords": [{
        "timeUnixNano": "1760000000000000000",
        "body": {"stringValue": "claude_code.user_prompt"},
        "attributes": [
          {"key": "session.id", "value": {"stringValue": "4c1f0d1e-0000-4000-8000-00000000c0de"}},
          {"key": "prompt.id", "value": {"stringValue": "prompt-1"}},
          {"key": "user.email", "value": {"stringValue": "dev@example.com"}},
          {"key": "prompt", "value": {"stringValue": "List the files, then delete the backups"}}
        ]
      }]
    }]
  }]
}
```

Each attribute `value` holds exactly one of `stringValue`, `boolValue`, `intValue` (a
64-bit integer, usually a string), `doubleValue`, `bytesValue` (base64), `arrayValue`
(`{"values": [value, ...]}`) or `kvlistValue` (`{"values": [{"key", "value"}, ...]}`).
`timeUnixNano` is nanoseconds since the Unix epoch, as a string or a number.

**After the export.** A 200 means the events are stored, not yet scanned. Zaun scans
them afterwards, so telemetry never blocks or changes what the agent does. The events of
each agent session are grouped into turns (a prompt and what followed it), and each turn
is scanned once as a prompt scan of the key's project: the same classifiers, controls,
billing and retention as `POST /v1/scan/prompt`, with the turn's earlier messages read
back at the cached rate. The ingest key's daily spending cap applies; a key over it
waits for the next UTC day without losing its events. Results show on
`GET /v1/violations` and in the console with `channel: ingest`, the source, the reported
user (hashed with your organization's key) and the agent's own session id; a control
set to Block or Redact is reported as what it would have done. Every scan of one agent
session shares a Zaun session, a version 8 UUID derived from the agent's session id
with a key private to the project. Content the agent did not send (a redacted prompt,
tool output without tracing, a value cut at both ends) is recorded on the scan as a
coverage gap rather than assessed as benign. Re-exporting events already received
scans and bills nothing again within three days. Unscanned events are kept three days,
then deleted; the source's health report counts the turns scanned, not scanned and
with nothing to scan.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Content-Encoding` | header | no | string; one of `gzip`, `identity` |  |

Request body (required): `application/x-protobuf`, string (binary)

Request body (required): `application/json`, [OtlpLogsExportRequest](#otlplogsexportrequest)

Responses:

| Status | Description |
| --- | --- |
| `200` | Accepted. The body is an OTLP export response in the request's encoding; partial success counts records dropped for timestamps out of range. string (binary) [OtlpExportResponse](#otlpexportresponse) |
| `401` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `403` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `413` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `415` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `422` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `429` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/ingest/otlp/v1/traces

Receive OpenTelemetry traces from an agent. Operation `exportTraces`. Authentication: `Authorization: Bearer` with `ingestKey` (zaun_ingest_<64 hex>) or `ingestKeyHeader`.

Authenticate with an ingest key in `x-zaun-ingest-key` (recommended: exporters parse
header lists as `key=value` pairs, and this form needs no space) or
`Authorization: Bearer`. The key's source and project decide where the telemetry goes;
no attribute, header or payload field can choose another. The key is checked before the
body is read.

Send `application/x-protobuf` or `application/json`, uncompressed or with
`Content-Encoding: gzip`. Limits: 4 MiB as sent; 32 MiB decompressed, and at most ten
times the compressed size above 4 MiB; 10,000 records; 1,000 resource or scope groups;
200,000 attribute values and 64 MiB of decoded text, counting the copies of span
attributes in span events; attribute values nested at most 8 levels; 64 KiB per
attribute key. Over any of those limits the request is refused whole with 413 and
counted on the source. A text attribute value or log body over 64 KiB does not refuse
the request: the start and the end of the value are kept (32 KiB each, cut on UTF-8
character boundaries, with a note between them of how many bytes were left out) and the
event is marked as partially captured, so a long pasted prompt does not drop the rest
of its batch. Zaun assesses the text it kept. Over the telemetry allowances it is 429 with `Retry-After`; when
the server's decoders are busy it is 503 with `Retry-After`. Events more than five minutes ahead of the
server clock or older than the project's result retention are dropped individually and
reported as partial success.

Each span and each span event counts as a record and becomes an event. Claude Code
exports a tool's output only here, as the `tool.output` span event, so a source that
should scan tool output needs traces as well as logs. Spans and logs of one agent
session join the same turns and the same Zaun session; see the logs route for what
happens after the export.

**Request shape** (`application/json`; protobuf carries the same message). Fields not
shown are accepted and ignored; attribute values take the forms listed on the logs
route, and full field notes are in `OtlpTracesExportRequest`.

```json
{
  "resourceSpans": [{
    "resource": {"attributes": [
      {"key": "service.name", "value": {"stringValue": "claude-code"}}
    ]},
    "scopeSpans": [{
      "spans": [{
        "name": "claude_code.tool",
        "startTimeUnixNano": "1760000002000000000",
        "attributes": [
          {"key": "session.id", "value": {"stringValue": "4c1f0d1e-0000-4000-8000-00000000c0de"}},
          {"key": "prompt.id", "value": {"stringValue": "prompt-1"}},
          {"key": "tool_use_id", "value": {"stringValue": "toolu_77"}}
        ],
        "events": [{
          "name": "tool.output",
          "timeUnixNano": "1760000003000000000",
          "attributes": [{"key": "content", "value": {"stringValue": "backup.tar notes.md"}}]
        }]
      }]
    }]
  }]
}
```

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Content-Encoding` | header | no | string; one of `gzip`, `identity` |  |

Request body (required): `application/x-protobuf`, string (binary)

Request body (required): `application/json`, [OtlpTracesExportRequest](#otlptracesexportrequest)

Responses:

| Status | Description |
| --- | --- |
| `200` | Accepted. The body is an OTLP export response in the request's encoding; partial success counts records dropped for timestamps out of range. string (binary) [OtlpExportResponse](#otlpexportresponse) |
| `401` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `403` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `413` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `415` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `422` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `429` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/ingest/otlp/v1/metrics

Accept and discard OpenTelemetry metrics. Operation `exportMetrics`. Authentication: `Authorization: Bearer` with `ingestKey` (zaun_ingest_<64 hex>) or `ingestKeyHeader`.

Authenticates and discards a metrics export, returning an empty OTLP response, so
exporters with metrics enabled do not report errors or retry. Nothing is stored, but
each export counts toward the telemetry request allowance.

Responses:

| Status | Description |
| --- | --- |
| `200` | Discarded. [OtlpExportResponse](#otlpexportresponse) |
| `401` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `403` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `413` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `429` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/sources

List telemetry sources. Operation `listSources`. Authentication: `Authorization: Bearer` with `adminBearer`.

Lists the project's telemetry sources, oldest first.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | The project's sources. array of [IngestSource](#ingestsource) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/projects/{id}/sources

Create a telemetry source. Operation `createSource`. Authentication: `Authorization: Bearer` with `adminBearer`.

Creates a telemetry source: one population of exporters of one family (for example a
Claude Code fleet) in this project. Ingest keys are issued for a source with
`scopes: ["ingest:write"]` and its `source_id`. The capture flags record what the
customer asserts the clients send; the health report shows what actually arrives.
For Claude Code, Codex and generic exporters they set the content flags in generated
configuration. Cowork's content capture is a per-device setting
(`otlpContentCapture`) that no generated file controls, so for Cowork the flags only
choose the install guidance.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |

Request body (required): `application/json`, [CreateSource](#createsource)

Responses:

| Status | Description |
| --- | --- |
| `201` | The source. [IngestSource](#ingestsource) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/sources/{sid}/health

Read a source's health report. Operation `getSourceHealth`. Authentication: `Authorization: Bearer` with `adminBearer`.

Reports what the source has sent: `waiting` until anything arrives, `receiving` while
events arrive without refusals, `degraded` when anything was refused in the last 15
minutes. Counts by event name and refusals by reason, each with what to change, cover
the last 15 minutes; content presence covers the last day; connectivity checks issued in
the last day show whether their nonce has arrived.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `sid` | path | yes | string (uuid) | The source ID. |

Responses:

| Status | Description |
| --- | --- |
| `200` | The report. [SourceHealth](#sourcehealth) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### POST /v1/admin/projects/{id}/sources/{sid}/connectivity-checks

Start a connectivity check. Operation `createConnectivityCheck`. Authentication: `Authorization: Bearer` with `adminBearer`.

Issues a nonce and the steps to send it through the source's client. The health report
marks the check satisfied, naming the event that carried it, when an event containing
the nonce arrives within 30 minutes. A redacted client never satisfies it, which is
itself the finding.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `sid` | path | yes | string (uuid) | The source ID. |

Responses:

| Status | Description |
| --- | --- |
| `201` | The check. [NewConnectivityCheck](#newconnectivitycheck) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

### GET /v1/admin/projects/{id}/sources/{sid}/config

Generate a source's client configuration. Operation `getSourceConfig`. Authentication: `Authorization: Bearer` with `adminBearer`.

Renders the client configuration for the source with `<ZAUN_INGEST_KEY>` in place of
the key, which the API never knows. Targets: `claude_managed_file` and
`claude_server_managed` for Claude Code, `cowork` for Claude Cowork, `codex` for Codex,
and `otel_generic` for any other exporter. `install` lists where the file goes and
how to turn it on. A target that does not serve the source's family returns 422. An
API deployed without its public URL (`SCAN_PUBLIC_URL`) returns 503
`management_disabled` rather than guess an endpoint.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | string (uuid) |  |
| `sid` | path | yes | string (uuid) | The source ID. |
| `target` | query | yes | string; one of `claude_managed_file`, `claude_server_managed`, `cowork`, `codex`, `otel_generic` |  |

Responses:

| Status | Description |
| --- | --- |
| `200` | The configuration. [SourceConfig](#sourceconfig) |
| `503` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Hooks

Coding-agent hooks that scan a prompt or tool call before it runs and answer in the agent's own format.

### POST /v1/hooks/claude-code

Scan a Claude Code hook call. Operation `claudeCodeHook`. Authentication: `Authorization: Bearer` with `scanKey` (zaun_<64 hex>).

Point Claude Code's HTTP hooks here to stop a prompt or a tool call before it runs.
Claude Code posts its own hook payload, and the reply is Claude Code's own hook JSON,
so no script runs on the laptop. Subscribe `UserPromptSubmit`, `PreToolUse` and
`PostToolUse`; any other event is answered `{}` without a scan.

**What is scanned.** `UserPromptSubmit` scans `prompt` as a user message.
`PreToolUse` scans one assistant tool call: `tool_use_id` as its id, `tool_name` as
the function and `tool_input` as its arguments. `PostToolUse` scans `tool_response`
as a tool message. Tool results are observed only, because the tool has already run;
a result longer than one message is scanned up to the limit. Fields this route does
not read are ignored, since Claude Code adds fields between releases.

**Sessions.** Each Claude Code session is one Zaun session: `session_uuid` is the
version 5 UUID of `"claude_code:" + session_id` in the namespace of your project's
ID, so every key of the project shares it and another project never does.

**Replies.** A block on a prompt answers `decision: "block"` with a `reason` shown to
the user. A block on a tool call answers `hookSpecificOutput.permissionDecision:
"deny"` with a reason Claude reads. Both reasons name the detected labels, the
containment and the `scan_uuid`. A block whose containment is `end_session` also
answers `continue: false`. Alert and Redact answer `{}` with a `systemMessage`
warning, since a hook cannot rewrite a prompt. Anything else answers `{}`.

**Time.** The route answers within 8 seconds, inside the 10 second hook timeout of
the generated settings. A scan still running then is answered allow with
`systemMessage: "Zaun did not answer in time"`; it keeps running and is stored and
billed like any other when it finishes. Errors keep their status and error envelope,
so your `onFailure` setting decides whether the action proceeds.

**Keys and billing.** The API key needs the `scan:write` scope; create one with only
that scope for hooks. Calls count against your organization's prompt allowance and
are billed like `POST /v1/scan/prompt`: new tokens at the scan rate and the session's
earlier tokens at the cached rate. Results carry `channel: hook`, `agent:
claude_code`, `hook_event`, `tool_name` and `client_session_id` in their metadata.
Like a prompt, a tool result's text is kept as activity, redacted before it is stored,
and shown to the project in Violations and session retrieval. A project that turns
activity retention off keeps none.

**Size.** A prompt or tool call larger than one scan accepts (131,072 characters,
counting a tool call's serialized input) is refused in Claude Code's format without a
scan, since an error would let it through a fail-open client. A longer tool result is
scanned up to that limit. The request body may be up to 20 MiB.

Request body (required): `application/json`, [ClaudeCodeHookInput](#claudecodehookinput)

Responses:

| Status | Description |
| --- | --- |
| `200` | Claude Code's hook JSON. An empty object lets the action proceed. [ClaudeCodeHookOutput](#claudecodehookoutput) |
| `default` | Error envelope; HTTP status and stable code are listed in docs/limits.md. The request UUID also appears in X-Request-Id. This is a support correlation ID and does not promise a retrievable result when persistence failed. Rate-limit denials include Retry-After rounded up to seconds and X-RateLimit-Remaining. Dependency capacity errors use Retry-After: 2. [ErrorEnvelope](#errorenvelope) |

## Schemas

### ModelList

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `object` | yes | `list` |  |
| `data` | yes | array of [Model](#model) |  |

### Model

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string | Exact alias accepted by POST /v1/responses. |
| `object` | yes | `model` |  |
| `created` | yes | `0` | No creation timestamp is tracked for configured aliases. |
| `owned_by` | yes | `zaun` | Model route managed by Zaun. |
| `zaun` | yes | object |  |
| `zaun.max_output_tokens` | yes | integer | Model-specific output limit enforced by this API. |
| `zaun.sampling` | yes | string; one of `independent`, `exclusive`, `disabled` | Exclusive allows temperature or top_p but never both. Disabled rejects both parameters and uses provider defaults. |
| `zaun.effort` | yes | object or null | The reasoning.effort levels this model accepts and the level it runs at when a request names none. Null means the model takes no effort and any reasoning.effort returns 422. |
| `zaun.effort.levels` | yes | array of [Effort](#effort) |  |
| `zaun.effort.default` | yes | [Effort](#effort) or null | Null when the provider does not document the model's default. |
| `zaun.pricing` | yes | [CompletionPrice](#completionprice) |  |

### Effort

How much work the model puts into the whole response, including private thinking. Not every model accepts every level.

Type: string; one of `low`, `medium`, `high`, `xhigh`, `max`

### CompletionPrice

Generation token rates in USD. Existing Zaun scan charges are additional. Rates are integer USD microdollars per million tokens; one USD is 1000000 microdollars.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `currency` | yes | `usd` |  |
| `input_micros_per_million` | yes | integer |  |
| `output_micros_per_million` | yes | integer |  |
| `cache_read_micros_per_million` | yes | integer |  |
| `cache_write_5m_micros_per_million` | yes | integer |  |
| `cache_write_1h_micros_per_million` | yes | integer |  |

### Metadata

Caller-asserted, at most 4096 bytes and depth 2. Reserved keys rejected. Never embedded, sent to T2 or logged raw. A channel key of hook, ingest or import is rejected; Zaun sets those itself for client hooks, telemetry and imports.

Type: object

### Message

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `role` | yes | string; one of `system`, `developer`, `user`, `assistant`, `tool` |  |
| `content` | yes | null or string or array of object (Text) or object (Image) | Text to scan, or parts. Use null for tool-only messages with tool_calls. At most 131072 characters including tool calls per message. User messages may carry image parts: PNG or JPEG data URLs, at most 4 per request and 3 MiB each decoded. Their recognized text is assessed in place and does not count toward the character cap. Each image costs USD 0.0075 for text recognition. Images elsewhere, remote URLs, and images where image input is not enabled return 422 unsupported_content. |
| `content[] (Text).type` | yes | `text` |  |
| `content[] (Text).text` | yes | string |  |
| `content[] (Image).type` | yes | `image_url` |  |
| `content[] (Image).image_url` | yes | object |  |
| `name` | no | string |  |
| `tool_call_id` | no | string |  |
| `tool_calls` | no | array of object |  |
| `tool_calls[].id` | yes | string |  |
| `tool_calls[].type` | yes | `function` |  |
| `tool_calls[].function` | yes | object |  |
| `tool_calls[].function.name` | yes | string |  |
| `tool_calls[].function.arguments` | yes | string | A JSON object encoded as a string. The entire arguments value is scanned. |

### PromptInput

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `sample` | no | boolean | Controls whether Zaun shows you this request's text. Zaun keeps a redacted copy of every request unless the project sets retain_activity to false. With sample=false the copy is kept but never returned by GET /v1/sessions or shown in the console. It is still used for session context and campaign detection, and classification and billing are unchanged. |
| `messages` | yes | array of [Message](#message) | Every supplied message is scanned, including text and tool calls, except messages this session has already scanned. Include only messages you want assessed. |
| `session_uuid` | no | string (uuid) | Groups related activity for later retrieval with GET /v1/sessions/{session_uuid}. The API assesses only messages this session has not already scanned and bills each call for the session's cumulative input tokens. Version 8 UUIDs are reserved for the sessions Zaun derives from agent telemetry and return 422. |
| `metadata` | no | [Metadata](#metadata) |  |

### ResponsesRequest

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `sample` | no | boolean | Controls whether Zaun shows you this request's text. Zaun keeps a redacted copy of every request unless the project sets retain_activity to false. With sample=false the copy is kept but never returned by GET /v1/sessions or shown in the console. It is still used for session context and campaign detection, and classification and billing are unchanged. |
| `model` | yes | string | An ID returned by GET /v1/models. |
| `input` | yes | string or array of [ResponsesInputItem](#responsesinputitem) | A user message as text, or conversation items including function calls and their outputs. |
| `instructions` | no | string or null | Inserted as the first system message. |
| `tools` | no | array of [ResponsesTool](#responsestool) |  |
| `tool_choice` | no | [ResponsesToolChoice](#responsestoolchoice) |  |
| `temperature` | no | number or null |  |
| `top_p` | no | number or null |  |
| `max_output_tokens` | no | integer or null |  |
| `reasoning` | no | object or null | Only effort is accepted. Reasoning summaries are never returned, so summary is rejected rather than ignored. |
| `reasoning.effort` | no | [Effort](#effort) or null | A level listed in the model's zaun.effort.levels. Omit it to run at zaun.effort.default. |
| `stream` | no | boolean or null |  |
| `store` | no | boolean or null; one of `false`, `null` | Responses are never stored. true is rejected. |
| `previous_response_id` | no | null | Conversation state is not retained. Send the full conversation in input instead. |
| `zaun` | no | [ZaunGenerationOptions](#zaungenerationoptions) |  |

### ResponsesInputItem

One of:

**Message**

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `type` | no | `message` |  |
| `role` | yes | string; one of `user`, `assistant`, `system` |  |
| `content` | yes | string or array of object (Text) or object (Image) | Text, or parts. People send input_text and input_image; replayed assistant turns use output_text. Images are PNG or JPEG data URLs, at most 4 per request and 3 MiB each decoded, accepted only by models marked vision. Their recognized text is assessed with the message text; the model receives the image. Remote URLs are not fetched. |
| `content[] (Text).type` | yes | string; one of `input_text`, `output_text` |  |
| `content[] (Text).text` | yes | string |  |
| `content[] (Text).annotations` | no | array of any |  |
| `content[] (Image).type` | yes | `input_image` |  |
| `content[] (Image).image_url` | yes | string | data:image/png;base64,... or data:image/jpeg;base64,... |
| `id` | no | string |  |
| `status` | no | string |  |

**Function call**

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `type` | yes | `function_call` |  |
| `call_id` | yes | string |  |
| `name` | yes | string |  |
| `arguments` | yes | string | JSON-encoded arguments. |

**Function call output**

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `type` | yes | `function_call_output` |  |
| `call_id` | yes | string |  |
| `output` | yes | string |  |

### ResponsesTool

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `type` | yes | `function` |  |
| `name` | yes | string |  |
| `description` | no | string |  |
| `parameters` | yes | object | JSON Schema whose type is object. |
| `strict` | no | boolean |  |

### ResponsesToolChoice

One of:

**Option 1**

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `type` | yes | `function` |  |
| `name` | yes | string |  |

Or: string; one of `none`, `auto`, `required`

### ZaunGenerationOptions

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan` | no | object |  |
| `scan.session_uuid` | no | string (uuid) or null |  |
| `provider_options` | no | map of object | Reserved. Omit it or send an empty object; any entry returns 422 invalid_request. No options are forwarded to a provider. |

### ResponsesResponse

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `request_uuid` | yes | string (uuid) | Persisted generation identifier. This is distinct from the X-Request-Id HTTP trace identifier. |
| `verdict` | yes | [ControlAction](#controlaction) |  |
| `id` | yes | string |  |
| `object` | yes | `response` |  |
| `created_at` | yes | integer |  |
| `status` | yes | string; one of `completed`, `incomplete` |  |
| `error` | no | null |  |
| `incomplete_details` | no | object or null |  |
| `incomplete_details.reason` | no | string; one of `max_output_tokens`, `content_filter` |  |
| `model` | yes | string |  |
| `output` | yes | array of object (Message) or object (Function call) |  |
| `output[] (Message).type` | yes | `message` |  |
| `output[] (Message).id` | yes | string |  |
| `output[] (Message).status` | yes | `completed` |  |
| `output[] (Message).role` | yes | `assistant` |  |
| `output[] (Message).content` | yes | array of object |  |
| `output[] (Message).content[].type` | yes | `output_text` |  |
| `output[] (Message).content[].text` | yes | string |  |
| `output[] (Message).content[].annotations` | yes | array of any |  |
| `output[] (Function call).type` | yes | `function_call` |  |
| `output[] (Function call).id` | yes | string |  |
| `output[] (Function call).call_id` | yes | string |  |
| `output[] (Function call).name` | yes | string |  |
| `output[] (Function call).arguments` | yes | string |  |
| `output[] (Function call).status` | yes | `completed` |  |
| `usage` | yes | object or null | Generation usage for the model you chose; null means unknown. Assessment usage stays in zaun.scan. |
| `usage.input_tokens` | yes | integer | Total model input including cache reads and writes. |
| `usage.input_tokens_details` | yes | object |  |
| `usage.input_tokens_details.cached_tokens` | yes | integer |  |
| `usage.input_tokens_details.cache_write_tokens` | yes | integer |  |
| `usage.output_tokens` | yes | integer |  |
| `usage.output_tokens_details` | yes | object |  |
| `usage.output_tokens_details.reasoning_tokens` | yes | integer or null | Provider-reported subset of output tokens. Null when the provider does not expose a separate count; never estimated. |
| `usage.total_tokens` | yes | integer |  |
| `instructions` | no | string or null |  |
| `tools` | yes | array of [ResponsesTool](#responsestool) |  |
| `tool_choice` | yes | [ResponsesToolChoice](#responsestoolchoice) |  |
| `parallel_tool_calls` | yes | boolean |  |
| `temperature` | no | number or null |  |
| `top_p` | no | number or null |  |
| `max_output_tokens` | no | integer or null |  |
| `reasoning` | no | object | The effort the request asked for, or null when it ran at the model default. |
| `reasoning.effort` | yes | [Effort](#effort) or null |  |
| `previous_response_id` | no | null |  |
| `store` | no | `false` |  |
| `truncation` | no | `disabled` |  |
| `text` | no | object |  |
| `metadata` | no | object |  |
| `zaun` | yes | object |  |
| `zaun.scan_uuid` | yes | string (uuid) | Input assessment ID for feedback. For regex-blocked requests, identifies the persisted preflight check. |
| `zaun.output_scan_uuid` | no | string (uuid) | Tool-call assessment ID for feedback. Omitted when no output assessment ran. |
| `zaun.price_version` | no | string (uuid) | Provider rate version pinned at admission. Omitted for legacy unpriced deployments. |
| `zaun.scan` | yes | [ScanResult](#scanresult) |  |
| `zaun.input_checks` | no | object | Input regex detections. Only IDs and the pinned action are retained. |
| `zaun.input_checks.controls` | yes | array of string; one of `pii_email`, `pii_phone`, `pii_ssn`, `pii_credit_card`, `pii_ip`, `credentials`, `zaun_test` | The input checks that matched. zaun_test marks the published test string ZAUN-TEST-VIOLATION-7f3a9c and always blocks. |
| `zaun.input_checks.action` | yes | string; one of `no_action`, `alert`, `redact`, `block` |  |
| `zaun.output_scan` | no | [ScanResult](#scanresult) | Generated tool-call assessment, omitted for plain text output. Block withholds calls and text and ends incomplete with content_filter; generation usage is retained. |
| `zaun.enforcement` | yes | string; one of `allowed`, `blocked`, `overridden` |  |
| `zaun.images` | no | object | Present only for requests that carried images. An image without recognized text was read; it contributed nothing to assess. Pixel-level and purely visual attacks are not assessed. |
| `zaun.images.total` | yes | integer |  |
| `zaun.images.with_text` | yes | integer |  |
| `zaun.accounting_status` | no | string; one of `recorded`, `pending` | Whether terminal state and available measured usage were saved. Pending does not authorize retry. |
| `zaun.activity_capture` | yes | object |  |
| `zaun.activity_capture.status` | yes | string; one of `requested`, `disabled` | Requested means asynchronous redacted capture was enabled for the project, whatever the request's sample value. Disabled means the project turned retain_activity off. It does not claim durable storage completed. |
| `zaun.activity_capture.retention_days` | yes | integer or null; one of `30`, `null` |  |
| `zaun.reason` | yes | string; one of `no_blocking_recommendation`, `policy_block`, `provider_refusal`, `project_risk_override`, `incomplete_assessment`, `assessment_mismatch` |  |

### BatchInput

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `items` | yes | array of object | Unique item_id values; at most 25 items. |
| `items[].item_id` | yes | string |  |
| `items[].messages` | yes | array of [Message](#message) | Every supplied message is scanned, including text and tool calls. Include only messages you want assessed. No conversation history is loaded. |
| `items[].session_uuid` | no | string (uuid) |  |
| `items[].metadata` | no | [Metadata](#metadata) |  |

### Verdict

Type: string; one of `benign`, `violation`, `unknown`

### Severity

How serious the classifier judged the message. Present on cascade_v1 results when the
served cascade configuration classifies by severity, and absent otherwise.

threat: the classifier found a threat. This is the only severity that can produce a
block. A control set to block blocks on a threat and alerts on every other severity.

risk_escalate: a violation that is not a threat. It alerts and never blocks. The same
value covers three cases that need a person's review: the classifier call failed, a
threat appeared only in assistant messages, or the deployment's confirmation reading did
not confirm a threat.

risk_monitor: a lower-impact risk. It alerts and never blocks.

benign: no violation.

A result can carry risk_escalate or risk_monitor with detection_verdict benign and no
recommendation. That means the deployment recorded the class without acting on it.
Read severity as the classifier's reading and verdict as the decision.

Type: string; one of `threat`, `risk_escalate`, `risk_monitor`, `benign`

### Containment

What to contain beside the action: our recommendation, and the action is yours. The
rules read the request's newest message, and the first that matches applies:

1. review: the classifier call failed, so the result is a violation for a person to check.
2. disable_tool: a block on an assistant tool call whose labels include Exfiltration or
   Credential Access. Disable the tool the call named.
3. end_session: a block the session's earlier signals carried
   (session.escalate_reason is session_boost). End the session.
4. stop_turn: any other block. Do not send the prompt, run the tool or use the reply.
5. review: severity risk_escalate on an assistant or tool message, which never blocks.
6. none: nothing to contain.

quarantine_session is reserved for telemetry ingestion and a scan never returns it.
Document scans, and results recorded before this field existed, are read as a user
message.

Type: string; one of `none`, `stop_turn`, `end_session`, `quarantine_session`, `disable_tool`, `review`

### Confidence

Confidence in the reported verdict, not P(violation), not cosine similarity. Null for unknown; uncalibrated in v1. Capped at 0.997.

Type: number or null

### ConfidenceBasis

Heuristic support for the verdict: t2_only uses a model verdict at 0.65; t2_null means unknown with null confidence. knn_heuristic_v1 names the basis a primary project pins in revisions.confidence_basis; it and t2_agreement_v1 and t2_disagreement_v1 appear only on results recorded by the retired reference classifier. These scores are not calibrated probabilities. cascade_v1 (classifier.mode cascade_v1) is the exception: confidence is the calibrated probability that the message warrants escalation, whatever the verdict.

Type: string; one of `knn_heuristic_v1`, `t2_agreement_v1`, `t2_disagreement_v1`, `t2_only`, `t2_null`, `cascade_v1`

### DetectionSampleCatalog

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `version` | yes | integer |  |
| `groups` | yes | array of object | Display groups, in the order the playground shows them. |
| `groups[].id` | yes | string |  |
| `groups[].label` | yes | string |  |
| `samples` | yes | array of [DetectionSample](#detectionsample) |  |

### DetectionSample

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string |  |
| `group` | yes | string | The id of one of the catalog's groups. |
| `title` | yes | string |  |
| `explains` | yes | string | One sentence on why the sample matters. |
| `messages` | yes | array of [Message](#message) | The exact `messages` of the scan request the sample sends. |
| `expected` | yes | object | The outcome under the self-serve default controls. An observed action at least as strong counts as a match, because behavioral classification may escalate further. |
| `expected.violation` | yes | boolean |  |
| `expected.action` | yes | string or null; one of `block`, `redact`, `alert`, `no_action`, `null` |  |

### Finding

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scheme` | yes | `mitre_atlas` |  |
| `version` | yes | `4` |  |
| `kind` | yes | string; one of `tactic`, `risk` | risk for Risky and Self-Harm, tactic for an ATLAS tactic label. |
| `id` | yes | string | ATLAS tactic label, Risky or Self-Harm; never represented as a technique ID. |
| `owasp_agentic` | yes | array of string |  |
| `confidence` | yes | [Confidence](#confidence) |  |
| `source` | no | string; one of `knn`, `t2`, `document_model`, `cascade` |  |
| `evidence` | yes | [Evidence](#evidence) |  |

### Evidence

Offsets and indices only; never quoted input text.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `message_index` | no | integer |  |
| `message_indices` | no | array of integer |  |
| `chunk_index` | no | integer |  |
| `page` | no | integer or null |  |
| `offset` | no | integer |  |
| `region_indices` | no | array of integer | Indices into extraction_evidence.regions overlapping the classified chunk, not exact attack locations. |
| `source` | no | string; one of `knn`, `t2`, `document_model`, `cascade` |  |

### Stages

Outcomes of each stage. embed and knn belong to the retired reference classifier and are always skipped on new results. Inspect these alongside status and coverage; skipped and bypassed stages are not successful full assessments.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `embed` | yes | string; one of `ok`, `bypassed_oversize`, `failed`, `skipped` |  |
| `knn` | yes | string; one of `ok`, `failed`, `skipped` |  |
| `t2` | yes | string; one of `ok`, `timeout`, `refused`, `throttled`, `invalid_output`, `error`, `skipped` |  |
| `t1` | no | string; one of `ok`, `unavailable` | Present for cascade_v1 projects only. unavailable means T1 could not score and the direct classifier decided. |
| `extraction` | no | string; one of `ok`, `failed` |  |
| `document_model` | no | string; one of `ok`, `truncated`, `timeout`, `refused`, `invalid_output`, `error`, `failed`, `skipped` |  |

### Signals

Assessment evidence. Model, corpus and routing identity are never returned. max_similarity, neighbor_count, adverse_share, support, margin and knn describe the retired reference classifier and appear only on results it recorded.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `max_similarity` | no | number or null |  |
| `neighbor_count` | no | integer |  |
| `adverse_share` | no | number or null |  |
| `support` | no | number or null |  |
| `margin` | no | number or null |  |
| `knn` | no | object | Supporting kNN evidence; never promoted to a conclusive verdict after T2 failure. |
| `knn.verdict` | no | [Verdict](#verdict) |  |
| `knn.violation` | no | boolean or null |  |
| `knn.confidence` | no | [Confidence](#confidence) |  |
| `knn.findings` | no | array of [Finding](#finding) |  |
| `t2` | no | object | Successful classifier output, including explicit abstention. |
| `t2.label` | no | string |  |
| `t2.verdict` | no | [Verdict](#verdict) |  |
| `document_model` | no | object |  |
| `document_model.verdict` | no | [Verdict](#verdict) |  |
| `document_model.partial` | no | boolean |  |

### Coverage

Assessed message or document chunk counts and extraction coverage. OCR PDF pages are counted locally; model-transcribed page counts are model-reported. Neither proves OCR fidelity; uncertain or truncated extraction and failed chunks cannot establish benign.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `messages_assessed` | yes | integer |  |
| `messages_total` | yes | integer |  |
| `chunks` | yes | integer |  |
| `chunks_assessed` | no | integer | Completed classifier windows for a chunked prompt or completion assessment. |
| `extraction` | no | string; one of `complete`, `uncertain`, `truncated`, `failed` |  |
| `extraction_source` | no | string; one of `document_model`, `document_text`, `ocr`, `office_parser`, `pdf_text_layer` | Original UTF-8 scored as uploaded (never transcribed), model transcription, the native Office parser (used ahead of the model for DOC/DOCX/XLS/XLSX), the PDF's own embedded text layer (used ahead of the model when it accounts for every page), or OCR. |
| `pages_extracted` | no | integer | OCR PDF page count; zero for Office files whose parsers do not render pagination; one logical page for native text; model-reported for the document model. Does not guarantee extraction fidelity. |
| `empty_pages` | no | array of integer |  |

### ExtractionEvidence

Text-free OCR evidence retained with the result. The OCR engine and its version are not returned. Coordinates are normalized page fractions with a top-left origin. Confidence describes OCR and geometry accuracy on a 0-100 scale, not security or coverage. Offsets refer to transient extracted UTF-8 text, which is not retained. Regions identify recognized content, not exact attack spans.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `regions` | yes | array of object |  |
| `regions[].kind` | yes | string; one of `line`, `word` |  |
| `regions[].page` | yes | integer |  |
| `regions[].start` | yes | integer | Inclusive UTF-8 byte offset. |
| `regions[].end` | yes | integer | Exclusive UTF-8 byte offset. |
| `regions[].confidence` | yes | number |  |
| `regions[].text_type` | no | string; one of `printed`, `handwriting` |  |
| `regions[].rotation_angle` | no | number |  |
| `regions[].bounding_box` | yes | object |  |
| `regions[].bounding_box.left` | yes | number |  |
| `regions[].bounding_box.top` | yes | number |  |
| `regions[].bounding_box.width` | yes | number |  |
| `regions[].bounding_box.height` | yes | number |  |
| `regions[].polygon` | yes | array of object |  |
| `regions[].polygon[].x` | yes | number |  |
| `regions[].polygon[].y` | yes | number |  |

### Revisions

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `pipeline` | yes | string | Opaque fingerprint of the classification pipeline. It changes whenever the models or reference corpus change and reveals nothing about either. |
| `confidence_basis` | yes | [ConfidenceBasis](#confidencebasis) |  |

### UsageAggregate

Type: [UsageTotals](#usagetotals) and object

### UsageTotals

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `input_tokens` | yes | integer (int64) | Tokens charged at the scan rate. |
| `cached_input_tokens` | yes | integer (int64) | Tokens charged at the cached rate: read back from an earlier call in the same session. |
| `output_tokens` | yes | integer (int64) | Generated tokens on the Responses route. |
| `calls` | yes | integer (int64) | Billed requests. |

### DocumentJob

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) | The queued result ID. Poll GET /v1/result/{scan_uuid}; feedback is accepted after assessment completes. |
| `status` | yes | string; one of `in_progress`, `failed` |  |
| `status_url` | no | string | Relative result URL for polling. |
| `job_state` | no | string; one of `queued`, `running` | Durable processing state while in progress. |
| `error` | no | [ErrorBody](#errorbody) |  |

### ClaudeCodeHookInput

The payload Claude Code posts to an HTTP hook. Fields this route does not read are ignored.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `session_id` | yes | string | Claude Code's session id; the Zaun session is derived from it. |
| `hook_event_name` | yes | string | UserPromptSubmit, PreToolUse and PostToolUse are scanned; any other event is answered {}. |
| `prompt` | no | string | UserPromptSubmit only. Required for that event. |
| `tool_name` | no | string | Tool events only. Required for them. |
| `tool_input` | no | any | Tool events only: the tool's arguments, scanned as the call's JSON arguments. |
| `tool_use_id` | no | string | Tool events only. Links a tool's call and result. |
| `tool_response` | no | any | PostToolUse only: the tool's result, scanned as a tool message. |

### ClaudeCodeHookOutput

Claude Code's hook JSON. An empty object lets the action proceed.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `continue` | no | `false` | Present when the containment is end_session: Claude stops. |
| `stopReason` | no | string | Shown to the user when continue is false. |
| `systemMessage` | no | string | A warning shown to the user on Alert or Redact, and when the scan did not finish in time. |
| `decision` | no | `block` | UserPromptSubmit only. The prompt is refused. |
| `reason` | no | string | UserPromptSubmit only: why, shown to the user. |
| `hookSpecificOutput` | no | object | PreToolUse only. The tool call is denied. |
| `hookSpecificOutput.hookEventName` | yes | `PreToolUse` |  |
| `hookSpecificOutput.permissionDecision` | yes | `deny` |  |
| `hookSpecificOutput.permissionDecisionReason` | yes | string | Why the call was denied; Claude reads it. |

### CompactScanResult

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) | Retained result ID. Use with GET or DELETE /v1/result/{scan_uuid} and POST /v1/scan/feedback. Idempotent retries preserve it; it is also returned as X-Scan-Request-Id. |
| `redacted_prompt` | no | string | Redacted rendered prompt text, present only on prompt scans when a Redact rule matches. Document scans never return extracted text. |
| `input_checks` | no | object |  |
| `input_checks.controls` | yes | array of string; one of `pii_email`, `pii_phone`, `pii_ssn`, `pii_credit_card`, `pii_ip`, `credentials`, `zaun_test` | The input checks that matched. zaun_test marks the published test string ZAUN-TEST-VIOLATION-7f3a9c and always blocks. |
| `input_checks.action` | yes | string; one of `no_action`, `alert`, `redact`, `block` |  |
| `completed_at` | yes | string (date-time) |  |
| `violation` | yes | boolean | Whether this assessment detected a violation. Completed classifier abstention is false; technical failures return errors. |
| `action` | yes | string or null; one of `null`, `no_action`, `alert`, `redact`, `block` | Null when no violation is detected. Otherwise the strongest configured action across detected controls; unconfigured controls use no_action, except self_harm, which uses the project's risky action until the project sets its own. |
| `session_uuid` | yes | string (uuid) or null | The optional session UUID supplied in the scan payload, or null. |
| `taxonomy` | yes | `mitre_atlas_tactic` |  |
| `technique` | yes | array of string | All detected tactic or risk labels, deduplicated and sorted. Empty when no violation is detected. |
| `usage` | yes | object | Tokens Zaun billing charges for this assessment: the input tokens of a completed result, split between new and cached, and zero for failed or inconclusive results and for results the classifier could not read (flagged for review), which are never charged. A resend with nothing new bills zero new tokens and the session's earlier tokens as cached. Output is always zero because assessments charge input only. Completion input and tool-call assessments are billed at the same rates, separately from generation, and report the tokens charged for them. Provider counts for models run on your behalf are never reported; results recorded before billed usage was retained report summed provider counts until they expire. |
| `usage.input_tokens` | yes | integer | Tokens this call assessed for the first time, billed at the scan rate. For a request without a session_uuid, all of its tokens. |
| `usage.cached_input_tokens` | yes | integer | Tokens the session assessed on an earlier call and the classifier read back on this one, billed at the cached rate (a tenth of the scan rate). Zero without a session. |
| `usage.output_tokens` | yes | integer |  |
| `session` | no | [SessionSummary](#sessionsummary) |  |
| `severity` | no | [Severity](#severity) |  |
| `containment` | yes | [Containment](#containment) |  |

### SessionSummary

Present only for a request that names its session_uuid. How the request related to the session's ledger.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `reconcile_mode` | yes | string; one of `new_session`, `full_history`, `full_history_diverged`, `window`, `delta` | new_session: no ledger yet. full_history: the request resent the history; what follows it is new. full_history_diverged: the resent history was edited part way; the ledger past the edit is replaced. window: a recent window was resent. delta: the request carries only new messages. |
| `messages_assessed` | yes | integer | Messages the ledger had not seen, which this call assessed. Zero for a resend with nothing new, which repeats the verdict of the session's last assessed message. |
| `messages_confirmed` | yes | integer | Request messages the ledger already held. |
| `messages_superseded` | yes | integer | Earlier messages an edited history replaced; they are no longer billed. |
| `cumulative_input_tokens` | yes | integer | The session's input tokens as it now stands: usage.input_tokens plus usage.cached_input_tokens. Informational; the usage object says what each part costs. |
| `context_supplied` | yes | boolean | Whether the classifier received the recent conversation beside the new messages. |
| `boost` | yes | number | How far the session's earlier weak signals lowered the escalation threshold, in log-odds. Zero until the first-stage classifier runs. |
| `escalate_reason` | yes | string or null; one of `null`, `message`, `session_boost` | Why the new messages were escalated, or null when they were not. |

### StoredResult

A result as stored, for operators: the outcome under its stored names (`request_uuid` for `scan_uuid`, the
detection `verdict` before a project's controls) and the diagnostics the public projection removes: every
stage's latency, each model attempt's usage, and the assessment's internal signals. Exactly these fields are
returned. It holds no message text: the signals hold scores, labels and counts. A stored completion object is
not a scan result and reads as not found.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `request_uuid` | yes | string (uuid) |  |
| `status` | yes | string; one of `completed`, `inconclusive`, `failed`, `in_progress` |  |
| `verdict` | yes | [Verdict](#verdict) or null |  |
| `violation` | yes | boolean or null |  |
| `severity` | no | [Severity](#severity) |  |
| `containment` | no | [Containment](#containment) | Recorded on prompt results; absent on documents and on results stored before the field existed. |
| `technique` | yes | string or null |  |
| `latency_ms` | yes | map of number or null | Milliseconds per stage (t1, t2) and in total. |
| `usage` | yes | array of object | One entry per model attempt that counts toward usage, the t1 stage excepted. |
| `usage[].stage` | yes | string |  |
| `usage[].model` | yes | string |  |
| `usage[].input_tokens` | yes | integer |  |
| `usage[].output_tokens` | yes | integer |  |
| `signals` | yes | object |  |
| `signals.cascade` | no | object |  |
| `signals.cascade.review` | no | `true` | Present when a failed or unscorable reading, not a score, made the result a violation for review. |
| `signals.cascade.stack` | no | object | The stacked decision's reading, present when the served model decides by its stack. |
| `signals.cascade.stack.score` | yes | number or null | Null when no vector could be scored. |
| `signals.cascade.stack.threshold` | yes | number |  |
| `signals.cascade.stack.review_band` | yes | number |  |
| `signals.cascade.stack.model` | no | string |  |
| `signals.cascade.stack.failed` | yes | boolean | A window that was sent did not read. |
| `signals.cascade.stack.windows` | yes | integer |  |
| `signals.cascade.stack.readable` | yes | integer |  |
| `signals.cascade.stack.refused` | no | boolean |  |
| `signals.cascade.stack.top_group` | no | string or null |  |
| `signals.cascade.stack.answers` | yes | map of number |  |
| `signals.cascade.stack.confirm` | no | object or null |  |
| `signals.cascade.stack.features` | yes | array of number |  |
| `signals.cascade.stack.item` | yes | object |  |

### ScanResult

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) | Retained result ID, the same value the scan response returned as scan_uuid. |
| `status` | yes | string; one of `completed`, `inconclusive`, `failed` |  |
| `stages` | yes | [Stages](#stages) |  |
| `verdict` | yes | [ControlAction](#controlaction) |  |
| `detection_verdict` | yes | [Verdict](#verdict) |  |
| `violation` | yes | boolean or null | True for violation, false for benign, null for unknown. |
| `technique` | yes | string or null | Primary finding at tactic level, Risky or Self-Harm. |
| `taxonomy` | yes | `mitre_atlas_tactic` |  |
| `findings` | yes | array of [Finding](#finding) |  |
| `recommendation` | yes | string or null; one of `alert`, `block`, `null` |  |
| `severity` | no | [Severity](#severity) |  |
| `containment` | yes | [Containment](#containment) |  |
| `confidence` | yes | [Confidence](#confidence) |  |
| `confidence_basis` | yes | [ConfidenceBasis](#confidencebasis) |  |
| `signals` | yes | [Signals](#signals) |  |
| `coverage` | yes | [Coverage](#coverage) |  |
| `extraction_evidence` | no | [ExtractionEvidence](#extractionevidence) |  |
| `config_version` | yes | integer |  |
| `revisions` | yes | [Revisions](#revisions) |  |
| `created_at` | yes | string (date-time) |  |
| `completed_at` | yes | string (date-time) |  |
| `expires_at` | yes | string (date-time) |  |
| `latency_ms` | yes | object | End-to-end processing time. Per-stage timings are not returned. |
| `latency_ms.total` | yes | number |  |
| `usage` | yes | object | Tokens Zaun billing charges for this assessment: input tokens of a completed result, and zero for failed or inconclusive results, which are never charged. Output is always zero because assessments charge input only. Completion input and tool-call assessments are billed at the same scan rate, separately from generation, and report the input tokens charged for them. Provider counts for models run on your behalf are never reported; results recorded before billed usage was retained report summed provider counts until they expire. |
| `usage.input_tokens` | yes | integer | Tokens assessed for the first time, billed at the scan rate. |
| `usage.cached_input_tokens` | yes | integer | Session tokens read back and billed at the cached rate; zero without a session. |
| `usage.output_tokens` | yes | integer |  |
| `sample` | no | object | Reference sampling status, kept for compatibility with the retired reference classifier. Always requested false, written 0 and skipped not_requested. |
| `sample.requested` | yes | boolean |  |
| `sample.written` | yes | integer | Always 0. |
| `sample.skipped` | yes | string or null; one of `not_requested`, `not_completed`, `verdict_unknown`, `no_embedding`, `store_unavailable`, `null` | Why nothing was written. `store_unavailable` never fails the scan; the verdict stands. |
| `error` | no | [ErrorBody](#errorbody) |  |
| `review_recommended` | no | boolean | Document scans only. True when the result should be routed to a person rather than acted on automatically. |
| `review_reason` | no | string; one of `partial_coverage`, `provider_refusal`, `extraction_failed` | Document scans only, present when review_recommended is true. partial_coverage means a chunk or the transcription did not complete; provider_refusal means the document model declined the content, which is a signal and not a clean result; extraction_failed means transcription timed out, was throttled, or returned nothing usable. |
| `per_chunk` | no | array of object |  |
| `per_chunk[].index` | yes | integer |  |
| `per_chunk[].offsets` | yes | object | UTF-8 byte offsets into extracted text; end is exclusive. |
| `per_chunk[].offsets.start` | yes | integer |  |
| `per_chunk[].offsets.end` | yes | integer |  |
| `per_chunk[].evidence` | yes | [Evidence](#evidence) |  |
| `per_chunk[].verdict` | yes | [Verdict](#verdict) |  |
| `per_chunk[].confidence` | yes | [Confidence](#confidence) |  |
| `per_chunk[].stages` | yes | [Stages](#stages) |  |

### BatchResult

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `items` | yes | array of object |  |
| `items[].item_id` | yes | string |  |
| `items[].scan_uuid` | yes | string (uuid) |  |
| `items[].result` | no | [ScanResult](#scanresult) |  |
| `items[].error` | no | [ErrorBody](#errorbody) |  |
| `summary` | yes | object |  |
| `summary.completed` | yes | integer |  |
| `summary.inconclusive` | yes | integer |  |
| `summary.failed` | yes | integer |  |

### ErrorEnvelope

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `error` | yes | [ErrorBody](#errorbody) |  |

### ErrorBody

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `code` | yes | string; one of `invalid_request`, `unsupported_content`, `input_too_long`, `idempotency_conflict`, `request_in_progress`, `not_found`, `result_expired`, `capacity`, `model_unavailable`, `result_not_persisted`, `extraction_unavailable`, `document_unreadable`, `no_text_extracted`, `deadline_exceeded`, `unauthorized`, `rate_limited`, `operation_not_implemented`, `database_unavailable`, `payload_too_large`, `unsupported_media_type`, `reference_not_ready`, `key_already_created`, `forbidden`, `policy_block`, `input_block`, `assessment_unavailable`, `insufficient_credits`, `key_daily_cap`, `billing_unavailable`, `precondition_failed`, `method_not_allowed`, `management_disabled`, `principal_lookup_unavailable`, `disabled` |  |
| `message` | yes | string |  |
| `request_uuid` | yes | string (uuid) |  |
| `stage` | no | string; one of `document_model` | Present when a pipeline stage failed inside an otherwise successful HTTP 200 result body (status failed). Absent from HTTP error envelopes, which describe request-level failures. |
| `key` | no | [KeyMetadata](#keymetadata) | Metadata returned on key_already_created retries; never a secret. |

### CreateMetadata

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | yes | string |  |
| `description` | no | string |  |

### CreateProject

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | yes | string |  |
| `description` | no | string |  |
| `config` | no | [Policy](#policy) |  |

### UpdateMetadata

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | no | string |  |
| `description` | no | string |  |
| `enabled` | no | boolean |  |

### Organization

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string (uuid) |  |
| `name` | yes | string |  |
| `description` | yes | string |  |
| `enabled` | yes | boolean |  |
| `created_at` | yes | string (date-time) |  |

### Project

Type: [Organization](#organization) and object

### ControlAction

Effective project action. Missing and newly added controls default to no_action, except self_harm, which follows the project's risky action until the project sets its own; the strongest matched action wins.

Type: string; one of `no_action`, `alert`, `redact`, `block`

### Policy

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `controls` | no | map of [ControlAction](#controlaction) | Zaun-owned control IDs mapped to actions. Rejects unknown IDs. Replaces legacy rules and disables risk_override when present. A project with no self_harm entry acts on its risky action for self-harm findings until it sets one; an explicit self_harm entry, including no_action, always wins. |
| `classifier` | no | object | The prompt classifier path, overriding the deployment default. cascade_v1 requires revisions.confidence_basis cascade_v1 when revisions are pinned. |
| `classifier.mode` | yes | string; one of `primary`, `cascade_v1` |  |
| `session_ledger` | no | boolean | Ignored. Every scan that names a session_uuid keeps a session ledger. Accepted so existing configurations still validate. |
| `risk_override` | no | boolean or null | Inference enforcement setting. True permits overriding a conclusive policy block, never incomplete assessment or failures. Absent/null/false in a published snapshot disables the override. Scan-only verdicts and recommendations are unchanged. |
| `context_prompt` | no | string |  |
| `taxonomy` | no | `mitre_atlas_tactic` |  |
| `rules` | no | array of object |  |
| `rules[].when` | yes | object |  |
| `rules[].when.verdict` | yes | [Verdict](#verdict) |  |
| `rules[].when.techniques` | no | array of string |  |
| `rules[].when.min_confidence` | no | number |  |
| `rules[].then` | yes | string; one of `alert`, `block` |  |
| `limits` | no | object | Legacy project settings retained for compatibility; admission uses organization limits. |
| `limits.prompt_rpm` | no | integer |  |
| `limits.prompt_tpm` | no | integer |  |
| `limits.document_rpm` | no | integer |  |
| `limits.document_tpm` | no | integer |  |
| `enabled_modalities` | no | array of string; one of `prompt`, `document` |  |
| `retain_input_days` | no | `0` | Nonzero retention is rejected until the cleanup worker exists. |
| `retain_activity` | no | boolean | Keep a redacted copy of each request's text for 30 days, for session context, behavioral correlation and campaign detection. Set false to keep none; turning it off weakens those features for this project. Requests that send sample=false are kept but hidden from you. |
| `result_ttl_days` | no | integer |  |
| `revisions` | no | [ConfigRevisions](#configrevisions) |  |

### ConfigRevisions

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `model` | yes | string |  |
| `corpus` | yes | string |  |
| `confidence_basis` | yes | string; one of `knn_heuristic_v1`, `cascade_v1` |  |

### Scopes

Type: array of string; one of `scan:write`, `run:write`, `result:read`, `result:delete`, `usage:read`, `violation:read`, `ingest:write`

### Violation

One summarized result, flat so clients can list and filter without parsing a result. Never carries message text.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) | The retained result, readable at GET /v1/violations/{scan_uuid}. |
| `session_uuid` | yes | string (uuid) or null |  |
| `client_session_id` | yes | string or null | The agent's own session id, when a client hook or telemetry source supplied one. |
| `scan_type` | yes | string; one of `prompt`, `document` |  |
| `activity_kind` | yes | string; one of `scan_input`, `completion_input`, `completion_output` |  |
| `created_at` | yes | string (date-time) |  |
| `completed_at` | yes | string (date-time) | When the assessment completed. Listings order by it and their interval applies to it. |
| `config_version` | yes | integer |  |
| `verdict` | yes | string; one of `benign`, `violation`, `unknown` | The detection verdict. |
| `action` | yes | string; one of `no_action`, `alert`, `redact`, `block` | The strongest action the API reported. |
| `severity` | yes | string or null; one of `threat`, `risk_escalate`, `risk_monitor`, `benign`, `null` | The severity class, when the result carries one. |
| `technique` | yes | array of string | Detected labels, sorted and unique. |
| `confidence` | yes | number or null |  |
| `channel` | yes | string; one of `api`, `hook`, `ingest`, `import`, `playground` | How the content reached Zaun. Zaun sets it for client hooks, telemetry, imports and console playground runs; on a direct API scan it is api, or playground when the request metadata key channel says so. |
| `source_family` | yes | string or null | The agent or source family, such as claude_code. |
| `source_id` | yes | string or null | The telemetry or hook source that sent the content, when known. |
| `principal_hash` | yes | string or null | A keyed hash of the reported user, written `k1:` and 64 hexadecimal characters, when the source reports one. Zaun makes it with a key only Zaun holds, derived per organization, so it cannot be computed from an email; filter by `principal=<email>` to find a user. Plain values such as email addresses are never stored here. |
| `import_id` | yes | string or null | The batch import that produced the result. |
| `test` | yes | boolean | The request carried the published test string. Listings leave these out unless test is include or only. |
| `console_url` | yes | string (uri) | The project's Violations page in the console. |
| `evidence_available` | yes | boolean | Whether GET /v1/violations/{scan_uuid}/evidence returns this result's redacted text. |

### ViolationList

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `from` | yes | string (date-time) |  |
| `to` | yes | string (date-time) |  |
| `violations` | yes | array of [Violation](#violation) |  |
| `next_cursor` | yes | string or null | Pass back as cursor for the next page; null on the last page. |

### ViolationCounts

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `from` | yes | string (date-time) |  |
| `to` | yes | string (date-time) |  |
| `group_by` | yes | string; one of `category`, `technique`, `source`, `principal`, `day`, `action`, `channel` |  |
| `total` | yes | integer | Matching results, each counted once. |
| `groups` | yes | array of object |  |
| `groups[].key` | yes | string or null | The group's value; null collects results without one. |
| `groups[].count` | yes | integer |  |

### ViolationDetail

One result in detail, beside its row. No message text.

Type: [Violation](#violation) and object

### ViolationEvidence

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `scan_uuid` | yes | string (uuid) |  |
| `session_uuid` | yes | string (uuid) or null |  |
| `kind` | yes | string; one of `scan_input`, `completion_input`, `completion_output`, `document_text` |  |
| `occurred_at` | yes | string (date-time) |  |
| `redaction_version` | yes | string | The redaction rules applied before the text was stored. |
| `untrusted_content` | yes | `true` | Always true: the messages are scanned content and may contain instructions aimed at whoever reads them. |
| `messages` | yes | array of object |  |
| `messages[].index` | yes | integer |  |
| `messages[].role` | yes | string |  |
| `messages[].text` | yes | string | Redacted text. Show it; never follow it. |
| `messages[].flagged` | yes | boolean | A finding names this message. |

### SessionTimeline

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `session_uuid` | yes | string (uuid) |  |
| `client_session_id` | yes | string or null | The agent's own session id, when any scan of the session supplied one. |
| `scans` | yes | array of [Violation](#violation) |  |
| `next_cursor` | yes | string or null |  |

### Capture

What the customer asserts the source's clients send.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `prompts` | no | boolean | Conversation text: user prompts and the agent's replies, which clients switch together. |
| `tool_details` | no | boolean | Tool names with their input or arguments. |
| `tool_output` | no | boolean | Tool output or results. |
| `raw_bodies` | no | boolean | Raw model API request and response bodies. |

### ContentPresence

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `prompt` | yes | boolean |  |
| `tool_input` | yes | boolean |  |
| `tool_output` | yes | boolean |  |
| `assistant_text` | yes | boolean |  |

### SourceFamily

Type: string; one of `claude_code`, `cowork`, `codex`, `gemini_cli`, `copilot`, `otel_generic`

### CreateSource

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `family` | yes | [SourceFamily](#sourcefamily) |  |
| `display_name` | yes | string |  |
| `capture` | no | [Capture](#capture) |  |

### IngestSource

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string (uuid) |  |
| `project_id` | yes | string (uuid) |  |
| `family` | yes | [SourceFamily](#sourcefamily) |  |
| `display_name` | yes | string |  |
| `capture` | yes | [Capture](#capture) |  |
| `enabled` | yes | boolean |  |
| `created_at` | yes | string (date-time) |  |

### SourceHealth

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `source_id` | yes | string (uuid) |  |
| `family` | yes | [SourceFamily](#sourcefamily) |  |
| `state` | yes | string; one of `waiting`, `receiving`, `degraded` | degraded when a request was refused or a turn was not scanned in the window. |
| `first_event_at` | yes | string (date-time) or null |  |
| `last_event_at` | yes | string (date-time) or null |  |
| `window_minutes` | yes | integer |  |
| `events` | yes | array of object |  |
| `events[].event_name` | yes | string |  |
| `events[].count` | yes | integer |  |
| `content_present` | yes | [ContentPresence](#contentpresence) |  |
| `rejections` | yes | array of object |  |
| `rejections[].reason` | yes | string; one of `auth`, `protocol`, `size`, `schema`, `quota` |  |
| `rejections[].count` | yes | integer |  |
| `rejections[].fix` | yes | string |  |
| `turns` | yes | object | What became of the conversation turns Zaun assembled from this source's events in the window. |
| `turns.scanned` | yes | integer | Turns scanned, or matched to an earlier scan of the same turn. |
| `turns.not_scanned` | yes | integer | Turns the scan refused, or that were never reached because their export kept failing. |
| `turns.nothing_to_scan` | yes | integer | Turns with no content to scan: the agent's logging sent none. |
| `connectivity_checks` | yes | array of object |  |
| `connectivity_checks[].id` | yes | string (uuid) |  |
| `connectivity_checks[].nonce` | yes | string |  |
| `connectivity_checks[].created_at` | yes | string (date-time) |  |
| `connectivity_checks[].expires_at` | yes | string (date-time) |  |
| `connectivity_checks[].satisfied_at` | yes | string (date-time) or null |  |
| `connectivity_checks[].satisfied_by` | yes | string or null |  |

### NewConnectivityCheck

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string (uuid) |  |
| `nonce` | yes | string |  |
| `expires_at` | yes | string (date-time) |  |
| `steps` | yes | array of string |  |

### SourceConfig

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `target` | yes | string; one of `claude_managed_file`, `claude_server_managed`, `cowork`, `codex`, `otel_generic` |  |
| `filename` | yes | string |  |
| `media_type` | yes | string |  |
| `content` | yes | string | The rendered file; the key is the placeholder. |
| `placeholders` | yes | array of string |  |
| `install` | yes | array of string |  |

### IngestSettings

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `project_id` | yes | string (uuid) |  |
| `enabled` | yes | boolean |  |
| `updated_at` | yes | string (date-time) or null | When the switch last changed; null while it has never been set. |

### OtlpLogsExportRequest

An OTLP/JSON `ExportLogsServiceRequest` (opentelemetry-proto v1, proto3 JSON mapping), the
body an OpenTelemetry SDK sends to `/v1/logs`. Field names may be lowerCamelCase or
snake_case. Integer fields of 64 bits (`timeUnixNano`, `intValue`) may be JSON strings or
numbers. Fields not listed here, such as `severityNumber`, `traceId`, `spanId` or `flags`,
are accepted and ignored.

Each log record becomes one event. Its name comes from the log body for Claude Code and
Cowork, the `event.name` attribute for Codex, and `eventName` for other producers. The
attributes Zaun reads for content, sessions and users are listed in the OpenTelemetry
guide.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `resourceLogs` | yes | array of object | One entry per producing process or service. |
| `resourceLogs[].resource` | no | [OtlpResource](#otlpresource) |  |
| `resourceLogs[].scopeLogs` | no | array of object | One entry per instrumentation library within the resource. |
| `resourceLogs[].scopeLogs[].scope` | no | [OtlpScope](#otlpscope) |  |
| `resourceLogs[].scopeLogs[].logRecords` | no | array of [OtlpLogRecord](#otlplogrecord) |  |

### OtlpTracesExportRequest

An OTLP/JSON `ExportTraceServiceRequest` (opentelemetry-proto v1, proto3 JSON mapping), the
body an OpenTelemetry SDK sends to `/v1/traces`. Naming and integer rules are those of
`OtlpLogsExportRequest`. Fields not listed here, such as `traceId`, `spanId`,
`parentSpanId`, `kind`, `endTimeUnixNano`, `links` or `status`, are accepted and ignored.

Each span and each span event becomes one event, and a span's attributes are copied onto
each of its events, so `session.id` on the span groups its events. Claude Code sends a
tool's output only here, as the `tool.output` span event.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `resourceSpans` | yes | array of object | One entry per producing process or service. |
| `resourceSpans[].resource` | no | [OtlpResource](#otlpresource) |  |
| `resourceSpans[].scopeSpans` | no | array of object | One entry per instrumentation library within the resource. |
| `resourceSpans[].scopeSpans[].scope` | no | [OtlpScope](#otlpscope) |  |
| `resourceSpans[].scopeSpans[].spans` | no | array of [OtlpSpan](#otlpspan) |  |

### OtlpResource

The producing service. Zaun reads `service.name` and `service.version` (or `app.version`), and a reported user (`user.email`, `enduser.id`, `user.name`, `process.owner`) when no record names one.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `attributes` | no | array of [OtlpKeyValue](#otlpkeyvalue) |  |

### OtlpScope

The instrumentation library. Accepted; not used to route or assess.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | no | string |  |
| `version` | no | string |  |

### OtlpLogRecord

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `timeUnixNano` | no | string or integer | When the event happened, in nanoseconds since the Unix epoch. 0 or absent falls back to observedTimeUnixNano, then to arrival. |
| `observedTimeUnixNano` | no | string or integer | When the SDK observed the event; used when timeUnixNano is absent. |
| `eventName` | no | string | The event name for producers other than Claude Code, Cowork and Codex. |
| `severityText` | no | string | Accepted; not used. |
| `body` | no | [OtlpAnyValue](#otlpanyvalue) |  |
| `attributes` | no | array of [OtlpKeyValue](#otlpkeyvalue) | The record's attributes. Content (prompts, tool input and output, replies, `gen_ai.*` messages), session ids and the reported user are read from here. |

### OtlpSpan

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `name` | no | string | The span becomes an event with this name. |
| `startTimeUnixNano` | no | string or integer | When the span started, in nanoseconds since the Unix epoch. |
| `attributes` | no | array of [OtlpKeyValue](#otlpkeyvalue) | Copied onto the span's own event and onto each of its span events. |
| `events` | no | array of [OtlpSpanEvent](#otlpspanevent) |  |

### OtlpSpanEvent

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `timeUnixNano` | no | string or integer | When the event happened, in nanoseconds since the Unix epoch. |
| `name` | no | string | The event name, for example `tool.output`. |
| `attributes` | no | array of [OtlpKeyValue](#otlpkeyvalue) |  |

### OtlpKeyValue

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `key` | yes | string | The attribute name. A key over 64 KiB refuses the request with 413. |
| `value` | no | [OtlpAnyValue](#otlpanyvalue) |  |

### OtlpAnyValue

Exactly one of the properties. A text value over 64 KiB keeps its first and last 32 KiB. Values nest at most 8 levels.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `stringValue` | no | string |  |
| `boolValue` | no | boolean |  |
| `intValue` | no | string or integer | A 64-bit integer, usually written as a string. |
| `doubleValue` | no | number or string | A double; `NaN` and `Infinity` arrive as strings. |
| `bytesValue` | no | string |  |
| `arrayValue` | no | object |  |
| `arrayValue.values` | no | array of [OtlpAnyValue](#otlpanyvalue) |  |
| `kvlistValue` | no | object |  |
| `kvlistValue.values` | no | array of [OtlpKeyValue](#otlpkeyvalue) |  |

### OtlpExportResponse

An OTLP export response in JSON form; empty on full success.

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `partialSuccess` | no | object |  |
| `partialSuccess.rejectedLogRecords` | no | string | Log records dropped, an int64 written as a string. |
| `partialSuccess.rejectedSpans` | no | string | Spans and span events dropped, an int64 written as a string. |
| `partialSuccess.errorMessage` | no | string |  |

### KeyClaimCreated

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `claim` | yes | object |  |
| `claim.id` | yes | string (uuid) |  |
| `claim.project_id` | yes | string (uuid) |  |
| `claim.name` | yes | string |  |
| `claim.scopes` | yes | [Scopes](#scopes) |  |
| `claim.key_expires_at` | yes | string (date-time) or null | When the key this claim mints will expire; null never expires. |
| `claim.expires_at` | yes | string (date-time) | When the claim stops working, ten minutes after it was issued. |
| `claim.url` | yes | string (uri) | The console page where the user reveals the key or approves a command line. |

### KeyClaimPending

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `status` | yes | `approval_pending` |  |
| `approval_url` | yes | string (uri) | Where the user approves this command line. |
| `user_code` | yes | string | Print it; the user types it on the console page to approve this command line. |
| `expires_at` | yes | string (date-time) |  |

### KeyMetadata

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `id` | yes | string (uuid) |  |
| `project_id` | yes | string (uuid) |  |
| `name` | yes | string |  |
| `display_prefix` | yes | string | Display only; mutations always use the full immutable key ID. |
| `scopes` | yes | [Scopes](#scopes) |  |
| `created_at` | yes | string (date-time) |  |
| `expires_at` | yes | string (date-time) or null |  |
| `revoked_at` | yes | string (date-time) or null |  |
| `last_used_at` | yes | string (date-time) or null |  |
| `source_id` | yes | string (uuid) or null | The telemetry source an ingest key is bound to; null for every other key. |
| `daily_cap_micros` | yes | [DailyCapMicros](#dailycapmicros) |  |
| `internal` | yes | boolean | True for a key Zaun created and uses itself, such as the Detection tests key, whose secret nobody holds. Revoking it turns the feature off for the project. |

### DailyCapMicros

The key's daily spending cap in microdollars (1000000 is USD 1), or null for none. Once
the scans the key settled on the current UTC day reach the cap, every scan, document,
hook and response call with the key returns 402 key_daily_cap until the next UTC
midnight. Spend counts each completed scan at the scan prices; failed scans cost
nothing. Reading results and usage is never capped.

Limits: generation spend on POST /v1/responses is not counted, only its assessments.
The check reads the spend before a call and the count advances when the call settles,
so calls running at the same time can overshoot the cap by what they settle. While the
counter cannot be read, a capped key is still admitted within a bound: all of Zaun's
servers together admit at most half of the cap beyond what the key had already spent,
never past the cap, plus calls already in flight, and then return 402 key_daily_cap. A
server that restarts during the outage starts its share again. The spend settled
meanwhile is added to the counter when it can be written again, and a key already known
to be at its cap stays refused.

Type: integer or null
