# Limits and errors

Use this guide to size requests and handle API responses. The Limits tab in your
organization’s Settings shows its current tier, model pricing and allowances. Request and token
budgets are shared across projects and API keys in the organization.

## Request limits

| Limit | Value | Behavior |
| --- | --- | --- |
| JSON HTTP request body | 1 MiB = 1,048,576 bytes | 413 `payload_too_large`, including streamed bodies. `POST /v1/scan/prompt` and `POST /v1/responses` allow 20 MiB so that image data URLs fit. |
| Document multipart body | 11 MiB = 11,534,336 bytes | 413 `payload_too_large`; includes file and form overhead. |
| Document count and file size | One file, 10 MiB = 10,485,760 bytes | 413 above the byte cap before a provider call; multiple files are invalid. |
| Document formats | pdf, csv, doc, docx, xls, xlsx, html, txt, md | Bytes determine the format: Office container signatures or UTF-8/content signatures for text. Unsupported formats return 415. Upload bytes, not a URL. |
| Messages per prompt | 1 to 64 | Empty or oversized lists return 422 `invalid_request`. |
| Characters per message | 131,072 across combined text parts and serialized tool calls | 422 `invalid_request` above the cap. Consecutive plain messages with the same role are supported. Text recognized in images does not count. |
| Message content | Text strings or text parts; `POST /v1/scan/prompt` and `POST /v1/responses` also accept image parts in user messages | Audio, file, and other part types return 422 `unsupported_content`. Images in non-user turns, sent to a model without vision, or sent where image input is not enabled return 422 `unsupported_content`. |
| Images per prompt scan or completion | 4 per request, 3 MiB each decoded, PNG or JPEG data URLs | Remote URLs and other formats return 422 `unsupported_content`; a malformed data URL, a signature that does not match the declared format, or an oversized image returns 422 `invalid_request`. Recognized text per image is capped at 32,768 characters (422 `input_too_long`). See [Scan an image](quickstart.md#scan-an-image). |
| Metadata | 4 KiB = 4,096 bytes, 32 top-level keys, depth 2 | 422 for oversized, overly nested, or reserved fields, and for a `channel` of `hook`, `ingest` or `import`, which only Zaun sets; never sent to models. |
| Project application context | 4,000 characters | Invalid policy publication is rejected. |
| Usage query interval | At most 90 days; default preceding 7 days | RFC 3339 `from` inclusive, `to` exclusive; invalid, empty, or reversed intervals return 422. |
| Violations listing | Interval at most 90 days, default the preceding 7; 1 to 100 rows a page; 1 to 100 count groups; 8 values per list filter; timelines 1 to 100 scans a page | `since` and `until` take RFC 3339 or a duration such as `24h`. Anything outside these bounds, or an unknown filter value, returns 422. |
| PDF pages | 20 | Split larger PDFs before uploading. |
| `POST /v1/responses` time to first output | 180 seconds. Non-streaming: the whole response, including input scan, generation, tool-call scan and accounting. Streaming: until the first text is released, which includes the input scan and any private thinking before the first word | 504 `deadline_exceeded`; generation the provider already ran is still charged. Stream long generations so only the first text has to arrive in time. At `reasoning.effort` `xhigh` or `max`, a model can think past 180 seconds before its first word, which fails even on a stream. |
| Extracted document text | 2 MiB UTF-8 | Reduce or split documents whose extracted text exceeds this limit. |

Use asynchronous document scans for background processing and poll the returned
status URL. See [Document scans](async-documents.md). For completions, use
`GET /v1/models` to discover the model-specific output cap for each enabled model.
Omitted completion output budgets default to 512 tokens.

## Hook limits

`POST /v1/hooks/claude-code` takes the same key and `scan:write` scope as a prompt scan.
Claude Code's generated hook settings give each call a 10-second timeout.

| Limit | Value | Behavior |
| --- | --- | --- |
| Answer deadline | 8 seconds | A scan still running then is answered 200 with allow and the message "Zaun did not answer in time". The scan finishes and is recorded. |
| Prompt or tool call size | 131,072 characters, counted as a scan counts them | A larger prompt or tool call is refused with a block in a 200 reply, never with an error, because an error lets a client that fails open skip the check. Split it into smaller steps. A tool result over the limit is scanned up to it. |
| Request body | 20 MiB = 20,971,520 bytes | 413 `payload_too_large`. |
| Request allowance | Shared with prompt scans | A hook call takes one prompt-scan admission and the same protected tokens, so a fleet's hooks and direct scans draw on one allowance per project and organization. When it is spent the call returns 429 `rate_limited`. |
| Payload | `session_id` and `hook_event_name` are required; `tool_input` and `tool_response` nest at most 128 levels | 422 `invalid_request`. |

## Telemetry ingestion limits

These apply to `POST /v1/ingest/otlp/v1/logs` and `/v1/ingest/otlp/v1/traces`, which accept
OpenTelemetry exports authenticated by an ingest key. Over any byte, record, count or nesting
limit the whole export is refused and counted on the key's source. A long text value is the
exception: it is kept at both ends and marked as partially captured, because exporters
do not retry a 413 and one long pasted prompt would otherwise drop the rest of its batch.

| Limit | Value | Behavior |
| --- | --- | --- |
| Export as sent | 4 MiB = 4,194,304 bytes | 413 `payload_too_large`, compressed or not. |
| Export after gzip | 32 MiB, and at most ten times its compressed size above 4 MiB | 413 `payload_too_large`. Send `Content-Encoding: gzip` or no encoding; other encodings return 415. |
| Records per export | 10,000 log records, spans and span events | 413 `payload_too_large`. Lower the exporter's batch size. |
| Attribute values per export | 200,000, nested values included | 413 `payload_too_large`. |
| Resource and scope groups per export | 1,000 | 413 `payload_too_large`. Exporters send a handful. |
| Decoded attribute text per export | 64 MiB, counting the copies of a span's attributes in each of its events | 413 `payload_too_large`. |
| Attribute nesting | 8 levels of arrays and key-value lists | 413 `payload_too_large`. |
| Attribute text value or log body | 64 KiB = 65,536 bytes | The export is accepted. A longer value keeps its first 32 KiB and its last 32 KiB, each cut on a UTF-8 character boundary, with a short note between them saying how many bytes were left out, and the event is marked as partially captured. Zaun assesses the text it kept, and the middle is not stored, so keep long content under the limit where it matters. Nested values and the text of a structured body are cut the same way. |
| Attribute key | 64 KiB = 65,536 bytes | 413 `payload_too_large` for the whole export. A key is a name, not content. |
| Identifier attributes | 256 bytes for session, prompt and tool call ids, `user.id`, the event sequence, and the service name and version | A longer value is accepted but not used as an identifier, so events carry no session, prompt or tool call id from it. |
| Content type | `application/x-protobuf` or `application/json` | 415 `unsupported_media_type`; a body that does not decode returns 422 `invalid_request`. |
| Event time | From the project's result retention ago to five minutes ahead | Records outside it are dropped alone; the 200 response reports them as OTLP partial success. |
| Decoding capacity | Eight exports decoding at once per server | 503 `capacity` with `Retry-After`; exporters retry. |

`POST /v1/ingest/otlp/v1/metrics` authenticates and discards metrics with 200.

Telemetry has its own allowances, separate from scan and completion allowances: a telemetry
burst never spends the organization's prompt requests or tokens.

| Allowance | Per project | Per organization |
| --- | --- | --- |
| Export requests per minute | 3,000 | 6,000 |
| Bytes as sent per minute | 256 MiB | 512 MiB |
| Events per day | 2,000,000 | 5,000,000 |

Over any of them the export is refused with 429 `rate_limited` and `Retry-After`, which
OpenTelemetry exporters honor; a request over the daily event allowance is refused whole and
nothing from it is stored. Metrics exports are discarded but still count as requests, so turn
metrics off in exporters that do not need them. An administrator can turn a project's telemetry off with
`PUT /v1/admin/projects/{id}/ingest`; its ingest keys are then refused with 401 before the body
is read.

## Organization allowances

**Protected tokens** are the tokens Zaun scans: submitted messages and tool calls,
extracted document text, and completion inputs and generated tool calls. They are
separate from model generation tokens.

| Tier | Scan/completion requests/min | Document requests/min | Scan/completion protected tokens/min | Document protected tokens/min |
| --- | --- | --- | --- | --- |
| 1 | 60 | 15 | 1,000,000 | 3,000,000 |
| 2 | 300 | 75 | 5,000,000 | 15,000,000 |
| 3 | 600 | 150 | 10,000,000 | 30,000,000 |

Both request and token allowances must permit the request. Creating additional
projects or keys does not increase the organization's allowance. Only Zaun
administrators can change tiers. Contact support to discuss higher limits.

Scanning costs $0.20 per million protected tokens assessed for the first time. In a
session, tokens assessed on an earlier call and read back by the classifier cost $0.02
per million (`usage.cached_input_tokens`). A result the classifier could not read, which
completes as a violation for review, is not billed. Document scanning costs
$1.00 per million protected tokens. Each image in a prompt scan or completion
costs $0.0075 for text recognition, on top of the protected tokens its recognized
text adds. Completion generation is billed separately at the model rates shown in
[Models](models.md). Prices do not vary by tier.

Prepaid packs: the $1,000 credit pack costs $800 (save 20%). It adds $1,000 of credit that
never expires and pays for scans and generation. At the pack rate that is $0.16 per million
protected tokens. Organization admins buy packs on the Billing tab in Settings. A refund of a
pack takes back the credit it paid for, in proportion: a partial refund takes back the same share
of the pack's credit. Any amount the pack's own credit no longer covers comes out of your other
credit first, and only then shows as a negative balance.

Each submission consumes a request admission, including retries. Idempotent scan
replays do not repeat assessment token usage or billing. Protected tokens exclude
internal classifier instructions and repeated window overlap. Completion input and
generated tool-call assessments share the scan token allowance; plain output text
is not automatically scanned. Completed scans with detections are billable.

Images are charged when the scan or completion assessment that read them completes,
the same as its tokens; failed or inconclusive assessments charge neither. Every image
in a request is read and charged, including images resent in a session conversation,
so send each image once. Idempotent replays do not charge images again.

## Edge limits

Before a request reaches the organization allowances above, the load balancer applies coarse
limits over five-minute windows:

| What is counted | Limit per 5 minutes |
| --- | --- |
| Requests with one API key (`Authorization: Bearer`, or `x-zaun-ingest-key` on ingest routes) | 10,000 |
| Requests with no key, per source address | 10,000 |
| All requests from one source address | 100,000 |

A gateway or proxy that forwards many keys from one address is limited per key, up to the
per-address ceiling. A request over an edge limit gets a `403` from the load balancer, not the
API's error envelope; wait for the window to pass before retrying.

## Retry guidance

For HTTP `429`, wait for `Retry-After` before retrying. A request exceeding the full
token allowance must be reduced or its organization's allowance increased.
For temporary `503` errors, honor `Retry-After` and retry with backoff.
Retry a prompt scan with the same `Idempotency-Key` and body: a completed scan replays its stored result
and is not billed again, including a session scan's new and cached tokens. A scan still running returns
`409 request_in_progress`; wait and retry with the same key. An attempt that failed was never
charged; retry it with a new key. A POST without a key starts a new request. Use the returned
`scan_uuid` (scans) or `request_uuid` (completions) to retrieve accepted work.
See [Responses](responses.md) for generation-specific retry behavior.

## Error catalogue

API errors use this envelope. For errors, `error.request_uuid` is also returned as
`X-Request-Id`; retain it when contacting support. Successful generation responses use
`request_uuid` for the persisted operation and `X-Request-Id` for the HTTP trace. A correlation ID does not
promise a retrievable result if persistence failed. Zaun always generates `X-Request-Id`; an
`X-Request-Id` you send comes back unchanged as `X-Client-Request-Id`.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request does not match the operation schema.",
    "request_uuid": "00000000-0000-4000-8000-000000000001"
  }
}
```

| Code | HTTP status | Meaning and next action |
| --- | --- | --- |
| `invalid_request` | 422 | Invalid input, unknown fields or invalid metadata. Correct the request before retrying. |
| `unsupported_content` | 422 | A message contains a part this endpoint, turn or model does not accept. Send text, send PNG or JPEG data URLs in user messages to `POST /v1/scan/prompt` or to a vision model on `POST /v1/responses`, or upload a supported document; remote resources are not fetched. |
| `input_too_long` | 422 | The input exceeds a supported token limit. Reduce or split the input. |
| `idempotency_conflict` | 409 | An `Idempotency-Key` was reused with a different body (scan, document, response or administrative operation). Send a new key for new content. |
| `request_in_progress` | 409 | The original request is still running. Honor `Retry-After` and retry with the same key and body. |
| `not_found` | 404 | Resource or route is absent, deleted, or outside this project. Check the endpoint, ID, and key's project. |
| `result_expired` | 410 | The result has expired. Submit a new request only if you intend a new assessment. |
| `capacity` | 503 | Processing is temporarily at capacity. Honor `Retry-After` and retry with backoff. |
| `model_unavailable` | 503 | Classification or generation is temporarily unavailable. Retry with backoff; contact support if it persists. |
| `extraction_unavailable` | 503 | Document extraction or image text recognition is temporarily unavailable. Retry with backoff. Nothing was dispatched to a model. |
| `result_not_persisted` | 500 | The result and usage could not be saved. No retrievable result is promised; contact support if persistent. |
| `document_unreadable` | 422 | The bytes could not be read before any provider work: an Office file the native parser rejected and the model then declined. Non-UTF-8 bytes that match no binary signature return 415 `unsupported_media_type` instead. A model refusal on the document-model path is instead reported inside an HTTP 200 body with `status: failed` and `error.stage: document_model`. Submit a readable supported variant. |
| `no_text_extracted` | 422 | The upload contains no text to assess (blank native text). An empty model transcription is instead reported inside an HTTP 200 body with `status: failed`, `stages.document_model: invalid_output` and `error.stage: document_model`. Submit readable text or another document representation. |
| `deadline_exceeded` | 504 | The operation exceeded its deadline. Retry with the original key or reduce new work. The reserved batch contract carries item failures inside HTTP 200. |
| `insufficient_credits` | 402 | Add organization credits before submitting more scans. |
| `key_daily_cap` | 402 | The key's settled spend today reached its daily cap. `zaun.resets_at` and `Retry-After` give the next UTC midnight, when the key works again; raise the cap with a new key if the work must continue today. The cap counts assessments, not generation on `POST /v1/responses`, and calls running at the same time can overshoot it by what they settle. If Zaun cannot read the key's spending counter, it keeps admitting the key 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 answer 402. A server that restarts during the outage starts its share again. The spend settled meanwhile is added to the counter when the counter is back, and a key already known to be at its cap stays refused. |
| `billing_unavailable` | 503 | Billing setup requires attention. Contact support. |
| `database_unavailable` | 503 | Authentication or result access is unavailable. Honor `Retry-After` and retry with backoff; stale authorization is not used. |
| `unauthorized` | 401 | Missing, invalid, expired, or revoked credential, or scans disabled for the project/organization. Check the key and project status with your operator. |
| `rate_limited` | 429 | Organization request or token allowance exhausted. Honor `Retry-After`. `X-RateLimit-Remaining` reports remaining request admissions for the current minute. |
| `operation_not_implemented` | 501 | This build reserves the operation. Batch scanning returns this code; use a supported operation. |
| `payload_too_large` | 413 | HTTP body or file byte cap exceeded. Reduce the upload or split the input. |
| `unsupported_media_type` | 415 | The uploaded document format is unsupported. Convert it to an accepted format. |
| `reference_not_ready` | 503 | Classification setup is not ready. Contact support with the request ID. |
| `key_already_created` | 409 | Key creation already committed and the secret cannot be returned again. If lost, ask the operator to revoke the returned key ID and issue a replacement. |
| `forbidden` | 403 | Recognized credential lacks the required scope or credential class. Use a correctly scoped key; customer keys cannot administer the service. Never returned for a content decision. |
| `policy_block` | 403 | The project's policy blocked the assessed input of a completion. The body carries the assessment in `zaun` with `enforcement: blocked` and `reason: policy_block`. Review the finding; retrying the same input returns the same decision. |
| `assessment_unavailable` | 503 | A completion's input or output assessment did not complete, so nothing was generated or released. Not a block: `zaun.enforcement` reports `blocked` only as the gate's fail-closed decision, with `reason: incomplete_assessment` or `assessment_mismatch`. Honor `Retry-After` and retry; contact support with the request ID if it persists. |
| `input_block` | 403 | An input regex control blocked a completion before any classifier or model call. `zaun.input_checks` names the controls. Remove or redact the matched content, or change the control's action. |
| `precondition_failed` | 412 | Published configuration changed. The operator must read its current version and retry publication with the matching `If-Match`. |
| `principal_lookup_unavailable` | 503 | Finding a user by email is not available. Filter by a hash exactly as results report it. Retrying does not help; contact support with the request ID. |
| `management_disabled` | 503 | Administrative access is unavailable. Contact support. |
| `disabled` | 409 | Organization or project is disabled. Enable it before issuing new keys. |
| `method_not_allowed` | 405 | The path exists but does not accept this method. Use the method documented in OpenAPI. |

The API reference lists the error codes returned by each operation.
Classification failures return errors. A completed classifier that abstains
returns `status: inconclusive`, `detection_verdict: unknown`, and `verdict: no_action`.
Benign PDF, DOC, DOCX, XLS and XLSX documents always take this shape over HTTP 200,
with `coverage.extraction: uncertain`: parser success is not a clearance guarantee, so
binary extraction can never clear a document, but it is not an error either. A detected
attack in such a document still reports `detection_verdict: violation`. Native TXT, MD,
CSV and HTML can report `complete` coverage and `status: completed`.
See [project controls](project-controls.md) for the action verdict and completion enforcement contract.


## Results and usage

Result reads and deletes are restricted to the key's project. Expired results return `410`;
missing or deleted results return `404`. Deleting a result does not erase usage
or billing history. Revoked keys retain historical attribution.

`GET /v1/usage` requires `usage:read`. Use UTC RFC 3339 `from` (inclusive) and `to`
(exclusive). Day is the only grouping. Empty intervals return zero
totals. Invalid ranges are rejected. Each day reports what billing charged:
`input_tokens` at the scan rate, `cached_input_tokens` at the cached rate,
`output_tokens` for generated text, and `calls`, the number of billed requests.

## Shared scan windows

Zaun assesses long inputs in overlapping windows and combines findings before
applying project controls. Original assessed text is counted once for the token
allowance. Chunking does not remove request limits. Incomplete assessments do not
establish benign clearance; failed tool-call checks withhold the generated calls.

Prompt scans assess all supplied messages, including assistant text and tool calls,
under the same protected-token limits and pricing. See [Project controls](project-controls.md) for actions.
