Zaun scan API
Assess prompts and documents, understand the verdict, and decide how your application responds.
Prompt and document scans are available in this build. Batch, proxy, and operator management routes remain reserved.
Quickstart
Get your first verdict in under five minutes once your Zaun operator has supplied
an active project key with scan:write and confirmed that the organization is
seeded and ready. Complete and document scans also need the service's model
access to be enabled. You need curl; Python is optional for the second half.
Set up once
Load your key from your secret store into ZAUN_API_KEY. The placeholder below
is not a working credential. Use HTTPS and keep the real secret out of source
control and logs.
export ZAUN_BASE_URL='https://api.zaunsecurity.com'
export ZAUN_API_KEY='zaun_scan_REPLACE_WITH_YOUR_KEY'
export FAST_ID="$(uuidgen)"
export COMPLETE_ID="$(uuidgen)"
export DOCUMENT_ID="$(uuidgen)"
printf '%s' 'Explain least privilege.' > example.txt
Keep each Idempotency-Key unchanged when retrying the same request after a lost
response. Generate a new key when you change the input or intentionally request
another scan. The examples below are copied from the checked-in OpenAPI request
and response examples. They illustrate a ready project with strong benign
reference matches and no matching policy rule; they are not live service
captures. IDs, timestamps, model/revision identifiers, usage, timing, scores,
and verdicts depend on your deployment and input.
1. Fast prompt
Fast compares your message with curated reference examples. This is the first call you need to make:
curl --silent --show-error --fail-with-body --max-time 15 \
"$ZAUN_BASE_URL/v1/scan/prompt" \
-H "Authorization: Bearer $ZAUN_API_KEY" \
-H "Idempotency-Key: $FAST_ID" \
-H 'Content-Type: application/json' \
--data-binary '{
"scan_type": "fast",
"messages": [
{
"role": "user",
"content": "Explain least privilege."
}
],
"target": "all"
}'
Example HTTP 200 response:
{
"request_uuid": "00000000-0000-4000-8000-000000000001",
"status": "completed",
"scan_type": "fast",
"stages": {
"embed": "ok",
"knn": "ok",
"t2": "skipped"
},
"verdict": "benign",
"violation": false,
"technique": null,
"taxonomy": "mitre_atlas_tactic",
"findings": [],
"recommendation": null,
"confidence": 0.9,
"confidence_basis": "knn_heuristic_v1",
"signals": {
"path": "close_benign",
"max_similarity": 0.98,
"neighbor_count": 20,
"adverse_share": 0,
"support": 0.9,
"margin": 1
},
"coverage": {
"messages_assessed": 1,
"messages_total": 1,
"chunks": 1
},
"config_version": 1,
"revisions": {
"model": "configured-revision",
"corpus": "configured-corpus",
"confidence_basis": "knn_heuristic_v1"
},
"created_at": "2026-09-16T00:00:00Z",
"completed_at": "2026-09-16T00:00:01Z",
"expires_at": "2026-10-16T00:00:00Z",
"latency_ms": {
"total": 61,
"embed": 38,
"search": 9,
"t2": null
},
"usage": []
}
2. Complete prompt
Complete always adds the second-stage classifier, even if the fast evidence
looks benign. The example shows agreement and stages.t2: ok.
curl --silent --show-error --fail-with-body --max-time 60 \
"$ZAUN_BASE_URL/v1/scan/prompt" \
-H "Authorization: Bearer $ZAUN_API_KEY" \
-H "Idempotency-Key: $COMPLETE_ID" \
-H 'Content-Type: application/json' \
--data-binary '{
"scan_type": "complete",
"messages": [
{
"role": "user",
"content": "Explain least privilege."
}
],
"target": "all"
}'
Example HTTP 200 response:
{
"request_uuid": "00000000-0000-4000-8000-000000000002",
"status": "completed",
"scan_type": "complete",
"stages": {
"embed": "ok",
"knn": "ok",
"t2": "ok"
},
"verdict": "benign",
"violation": false,
"technique": null,
"taxonomy": "mitre_atlas_tactic",
"findings": [],
"recommendation": null,
"confidence": 0.97,
"confidence_basis": "t2_agreement_v1",
"signals": {
"path": "close_benign",
"max_similarity": 0.98,
"neighbor_count": 20,
"adverse_share": 0,
"support": 0.9,
"margin": 1,
"knn": {
"verdict": "benign",
"confidence": 0.9
}
},
"coverage": {
"messages_assessed": 1,
"messages_total": 1,
"chunks": 1
},
"config_version": 1,
"revisions": {
"model": "configured-revision",
"corpus": "configured-corpus",
"confidence_basis": "knn_heuristic_v1"
},
"created_at": "2026-09-16T00:00:00Z",
"completed_at": "2026-09-16T00:00:02Z",
"expires_at": "2026-10-16T00:00:00Z",
"latency_ms": {
"total": 1261,
"embed": 38,
"search": 9,
"t2": 1200
},
"usage": [
{
"stage": "t2",
"model": "configured-t2-model",
"input_tokens": 620,
"output_tokens": 12
}
]
}
3. Upload a document
Upload the example.txt created above. Its bytes are exactly Explain least privilege., matching the OpenAPI multipart example. Curl sets the multipart
boundary, so do not add a JSON Content-Type header. The API accepts one file
up to 4,718,592 bytes (4.5 MiB); see limits for formats and coverage.
curl --silent --show-error --fail-with-body --max-time 110 \
"$ZAUN_BASE_URL/v1/scan/document" \
-H "Authorization: Bearer $ZAUN_API_KEY" \
-H "Idempotency-Key: $DOCUMENT_ID" \
-F 'scan_type=complete' \
-F 'file=@example.txt;type=text/plain'
Example HTTP 200 response:
{
"request_uuid": "00000000-0000-4000-8000-000000000003",
"status": "completed",
"scan_type": "complete",
"stages": {
"embed": "ok",
"knn": "ok",
"t2": "ok",
"document_model": "ok"
},
"verdict": "benign",
"violation": false,
"technique": null,
"taxonomy": "mitre_atlas_tactic",
"findings": [],
"recommendation": null,
"confidence": 0.97,
"confidence_basis": "t2_agreement_v1",
"signals": {
"path": "close_benign",
"max_similarity": 0.98,
"neighbor_count": 20,
"adverse_share": 0,
"support": 0.9,
"margin": 1,
"knn": {
"verdict": "benign",
"confidence": 0.9
},
"document_model": {
"verdict": "benign",
"partial": false
}
},
"coverage": {
"messages_assessed": 1,
"messages_total": 1,
"chunks": 1,
"extraction": "complete",
"extraction_source": "document_model",
"pages_extracted": 1,
"empty_pages": []
},
"config_version": 1,
"revisions": {
"model": "configured-revision",
"corpus": "configured-corpus",
"confidence_basis": "knn_heuristic_v1"
},
"created_at": "2026-09-16T00:00:00Z",
"completed_at": "2026-09-16T00:00:04Z",
"expires_at": "2026-10-16T00:00:00Z",
"latency_ms": {
"total": 3261,
"embed": 38,
"search": 9,
"t2": 1200,
"document_model": 2000
},
"usage": [
{
"stage": "document_model",
"model": "configured-document-model",
"input_tokens": 180,
"output_tokens": 60
},
{
"stage": "t2",
"model": "configured-t2-model",
"input_tokens": 620,
"output_tokens": 12
}
],
"per_chunk": [
{
"index": 0,
"offsets": {
"start": 0,
"end": 24
},
"evidence": {
"chunk_index": 0,
"page": 1,
"offset": 0
},
"verdict": "benign",
"confidence": 0.97,
"stages": {
"embed": "ok",
"knn": "ok",
"t2": "ok"
}
}
]
}
The document example reports one extracted page and one assessed chunk.
per_chunk locates evidence without returning extracted text. Document bytes
are sent to Bedrock even if you choose fast; see data handling.
The same three calls in Python
Use the same ZAUN_API_KEY and ZAUN_BASE_URL environment variables. Install
requests in a virtual environment:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install requests
Save this as quickstart.py and run python3 quickstart.py. It prints each full
response in the same shape as the curl examples. Python generates three new
request IDs, so these are new scans. For production retries, save the generated
idempotency key and reuse it with the same request body.
import json
import os
from uuid import uuid4
import requests
base_url = os.environ.get("ZAUN_BASE_URL", "https://api.zaunsecurity.com").rstrip("/")
client = requests.Session()
client.headers["Authorization"] = f"Bearer {os.environ['ZAUN_API_KEY']}"
fast_id = str(uuid4())
complete_id = str(uuid4())
document_id = str(uuid4())
# 1. Fast prompt.
fast = client.post(
f"{base_url}/v1/scan/prompt",
headers={"Idempotency-Key": fast_id},
json={
"scan_type": "fast",
"messages": [{"role": "user", "content": "Explain least privilege."}],
"target": "all",
},
timeout=(5, 15),
)
print(json.dumps(fast.json(), indent=2))
fast.raise_for_status()
# 2. Complete prompt.
complete = client.post(
f"{base_url}/v1/scan/prompt",
headers={"Idempotency-Key": complete_id},
json={
"scan_type": "complete",
"messages": [{"role": "user", "content": "Explain least privilege."}],
"target": "all",
},
timeout=(5, 60),
)
print(json.dumps(complete.json(), indent=2))
complete.raise_for_status()
# 3. Upload exactly the same bytes as example.txt, without needing a local file.
document = client.post(
f"{base_url}/v1/scan/document",
headers={"Idempotency-Key": document_id},
data={"scan_type": "complete"},
files={"file": ("example.txt", b"Explain least privilege.", "text/plain")},
timeout=(5, 110),
)
print(json.dumps(document.json(), indent=2))
document.raise_for_status()
Read the verdict before acting
HTTP 200 means an assessment result was returned, including inconclusive
results. Check status, verdict, confidence_basis, coverage, and stages.
A block recommendation is still HTTP 200; your application must enforce it.
A freshly seeded, ready organization can return verdict: unknown. This
means its reference examples do not provide enough clear, independent evidence
for your particular input. violation and confidence are then null. It is
neither a benign verdict nor proof of a failed setup. Try complete for a
contextual assessment and choose an explicit application policy for unknown.
Repeated scans do not add examples to the reference set. An organization that
has not been seeded or is not ready instead returns 503 reference_not_ready;
ask your operator to finish onboarding.
401 means the credential was not accepted; 403 means it lacks permission.
For 429, wait the Retry-After seconds before retrying with the same idempotency
key and body. See keys, concepts, and the complete
error catalogue. Retained results can be read at
GET /v1/result/{request_uuid} with result:read, normally for 30 days.
Concepts
A scan assesses the content you send and returns a verdict, supporting findings, and an optional recommendation. Your application decides what happens next. Start with the quickstart, then use these distinctions when deciding which results to allow, review, or block.
Fast and complete
| Mode | What it assesses | When to use it |
|---|---|---|
fast |
Compares selected messages with a curated set of labelled examples. It does not call the second-stage classifier, called T2 in results. | Quick assessment when the reference examples provide enough evidence. |
complete |
Performs the fast assessment, then always asks T2 to classify the full selected content using your project's application context. T2's valid label decides the final verdict. | More context-sensitive assessment, including content unfamiliar to the reference set. |
Send scan_type explicitly for prompts. By default, target: all assesses every
message, including system, assistant, and tool messages. Use target: last_user
only when you intend to assess the last user message alone. Send the conversation
you want assessed on every call: session_uuid is a correlation ID and does not
save conversation history. Instructions inside submitted content are themselves
assessed; scans do not execute tool calls or fetch linked resources.
Documents first go to a document model for text extraction and a document-level
assessment, even with scan_type: fast. Every extracted chunk receives the fast
assessment. Complete documents additionally send up to five selected chunks to
T2. Inspect per_chunk, coverage, and stages alongside the overall verdict.
Verdict and unknown
verdict |
violation |
Meaning |
|---|---|---|
benign |
false |
The available evidence supports ordinary, legitimate activity. |
violation |
true |
The available evidence supports an adverse category. |
unknown |
null |
There is not enough reliable evidence to choose either verdict. |
unknown prevents missing evidence from becoming a false assurance. A fast scan
can return it when the examples are too distant, too few independent examples
match, or benign and adverse evidence is too evenly split. A freshly seeded,
ready organization can therefore return unknown for unfamiliar content. Scans
do not teach the reference set, so repeating the request does not build support.
An organization whose reference set is not ready instead returns HTTP 503
reference_not_ready.
Content that exceeds the embedding window bypasses the fast comparison. The embedding representation also has a 2,000-character head/tail elision limit; content that would lose text at this boundary bypasses embedding too. A fast scan then cannot establish a benign verdict from that content. Complete can still assess the full selected text within its input limit. See limits.
If T2 times out, refuses, or returns unusable output, a complete prompt scan is
inconclusive with verdict: unknown and confidence: null. Its earlier fast
evidence remains in signals.knn; it is not a completed complete assessment.
Your application should define a separate action for unknown, inconclusive,
and unavailable scans.
Confidence
confidence describes support for the reported verdict. A benign result
with confidence 0.90 expresses strong support for benign activity. It does not
mean a 90% chance of a violation. These scores are heuristics, not calibrated
probabilities, accuracy guarantees, or raw similarity scores. unknown has
confidence: null, not zero.
Read confidence_basis with the number. The contract includes these three
comparison outcomes, plus two cases where comparison cannot settle the result:
confidence_basis |
Plain-language meaning |
|---|---|
knn_heuristic_v1 |
Fast evidence only. Close, consistent matches from independent reference groups strengthen the score; duplicate examples do not add independent support. |
t2_agreement_v1 |
T2 and fast evidence agree on benign versus adverse. The score starts at 0.70 and rises with fast support, up to 0.997. Agreement need not mean the same adverse category. |
t2_disagreement_v1 |
T2 and fast evidence disagree on benign versus adverse. T2 decides the verdict, with confidence 0.60. |
t2_only |
T2 or the document model supplies a verdict without a usable fast verdict. Confidence is 0.65. |
t2_null |
The classifier cannot supply a usable verdict. Confidence is null; check stages for a refusal, timeout, or other failure. |
Each finding has its own confidence. The top-level score supports the overall
verdict, not every category in findings. A low-confidence adverse finding can
coexist with a benign verdict. Language, encoding, and obfuscation coverage
should be evaluated on your own representative inputs; English examples do not
establish coverage for every language.
Categories and the OWASP Agentic crosswalk
findings[] contains the categories and evidence locations. technique names
the primary adverse finding for convenience. Despite that field name, the
ATLAS values below are tactics, not technique IDs:
taxonomy: mitre_atlas_tactic, scheme: mitre_atlas, kind: tactic.
Evidence points to messages, pages, or chunk offsets without quoting your input.
The API's versioned catalogue contains the following labels. Its
owasp_agentic values are a draft Zaun crosswalk, not an OWASP-endorsed
equivalence. An empty list means there is no direct mapping; it does not mean
the finding is harmless.
| ATLAS tactic | What it describes | OWASP Agentic IDs |
|---|---|---|
| Reconnaissance | Probing a target's AI systems before an attack. | ASI02 |
| Resource Development | Preparing attacker-controlled packages, datasets, or model files. | ASI04 |
| Initial Access | Introducing attacker-controlled instructions through prompts, documents, or tools. | ASI01, ASI04 |
| ML Model Access | Gaining unauthorized query or inference access to a model. | ASI03 |
| Execution | Causing attacker-controlled code or actions to run. | ASI02, ASI05 |
| Persistence | Maintaining unauthorized access across sessions. | ASI04, ASI06 |
| Privilege Escalation | Obtaining permissions beyond those needed for the task. | ASI03 |
| Defense Evasion | Hiding malicious activity or disabling protections. | ASI09, ASI10 |
| Credential Access | Collecting secrets for use outside the authorized task. | ASI03 |
| Discovery | Enumerating security-sensitive accounts, permissions, or systems. | ASI02, ASI03 |
| Collection | Gathering and staging target data for removal. | ASI02 |
| ML Attack Staging | Preparing adversarial inputs or poisoned data for a downstream model. | ASI06 |
| Exfiltration | Sending data to an unauthorized destination. | ASI02, ASI03, ASI07 |
| Impact | Destructively changing production data or systems. | ASI05, ASI08 |
| Supply Chain | Introducing untrusted or poisoned dependencies, skills, or tools. | ASI04 |
| Resource Abuse | Consuming costly APIs or compute far beyond the task's needs. | ASI02, ASI08 |
Risky covers concerning behavior that does not fit a named tactic, including
project-policy concerns without an ATLAS equivalent. It is adverse, appears as
technique: Risky with kind: risk, and has no OWASP mapping. Benign and Null
are classifier labels for benign activity and insufficient evidence; neither is
an adverse finding. Legitimate discussion of attacks is assessed in context,
not treated as a violation simply because it contains an attack name.
Policy and recommendation
Your project's published policy can match the final verdict, selected categories, and a minimum confidence. For example:
{
"rules": [
{
"when": {"verdict": "violation", "techniques": ["Exfiltration", "Credential Access"], "min_confidence": 0.85},
"then": "block"
},
{"when": {"verdict": "violation", "min_confidence": 0.60}, "then": "alert"},
{"when": {"verdict": "unknown"}, "then": "alert"}
]
}
Category rules compare each matching finding's confidence with the threshold,
including equality, using the unrounded value. They do not substitute the
overall verdict confidence for a category's score. block takes precedence
over alert; no match yields recommendation: null. Unknown matches only a
rule that explicitly names verdict: unknown.
Recommendations are advisory. block still returns HTTP 200 because the scan
succeeded; your application enforces the decision. alert does not create a
Zaun alert automatically. One configuration version applies to the whole scan
and is recorded in config_version.
Coverage and stages
status describes whether the promised assessment finished: completed,
inconclusive, or failed. Read it independently of verdict. In particular,
a document can retain a violation finding while coverage is incomplete.
coverage.messages_assessed and messages_total show the assessed message count
for prompts and chunk count for documents; chunks reports the chunk count.
With target: last_user, earlier messages are outside the requested assessment.
For documents, extraction_source: document_model, pages_extracted, and
empty_pages are model-reported, not an independent verification that every
page was read. extraction: truncated means extraction hit its output limit.
Partial extraction or a failed chunk cannot justify a benign result.
stages records each stage's outcome: embed for text comparison preparation,
knn for reference matching, t2 for the second-stage classifier, and
document_model for document extraction and assessment. ok means it ran
successfully; skipped means it did not run. bypassed_oversize explains an
embedding bypass. Timeout, refusal, throttling, invalid output, and failure
values identify the stage that prevented completion. See the
API reference for every value.
Limits and errors
Prompt scans (fast and complete), document scans, retained-result access,
and usage queries are implemented in this checkout. Batch, proxy, and operator
management routes are reserved and currently return 501. Their planned caps
remain listed here so clients can distinguish the contract from availability.
Input and execution limits
These are service defaults. Your operator can publish stricter project limits.
Byte and character caps reject oversized requests; an embedding-window bypass
instead affects the scan's evidence and is reported in stages.
| Limit | Default | Behavior |
|---|---|---|
| JSON HTTP request body | 1 MiB = 1,048,576 bytes | 413 payload_too_large, including streamed bodies. |
| Document multipart body | 5 MiB = 5,242,880 bytes | 413 payload_too_large; includes file and form overhead. |
| Document count and file size | One file, 4.5 MiB = 4,718,592 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; 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 | 32,768 across combined text parts | 422 invalid_request above the cap. |
| Message content | Text strings or text parts only | Image, audio, file, and other non-text parts return 422 unsupported_content. |
| Metadata | 4 KiB = 4,096 bytes, 32 top-level keys, depth 2 | 422 for oversized, overly nested, or reserved fields; never sent to models. |
| Project application context | 4,000 characters | Invalid policy publication is rejected. |
| Embedding window | 4,096 tokens per rendered message | Oversize bypasses embedding; fast cannot establish benign from bypassed content, complete can use full-text T2. |
| Embedding text representation | 2,000-character head/tail elision boundary | Text that would be elided also bypasses embedding, even below the token window. |
| Full prompt input | 32,000 tokens including rendered messages and project context | 422 input_too_long before paid calls, on both fast and complete. Split the input. |
| Document extraction output | 16,000 tokens | Output exhaustion returns inconclusive with coverage.extraction: truncated. |
| Document chunk size and overlap | 2,048 tokens, 256-token overlap | Every chunk receives fast assessment; incomplete chunk assessment prevents a benign conclusion. |
| Document T2 candidates | Up to 5 selected chunks on complete | Document-level assessment is also considered when extraction completes. |
| Fast prompt deadline | 5 seconds | Deadline failure is never a benign fallback. |
| Complete prompt deadline | 45 seconds; T2 stage timeout 40 seconds | Failed T2 returns HTTP 200 with an inconclusive result and the stage reason. |
| Document deadline | 90 seconds | 504 deadline_exceeded on operation timeout. |
| Batch deadline (reserved) | 60 seconds total | Ordered per-item outcomes; unfinished items carry deadline_exceeded. |
| Outer HTTP timeout | 120 seconds including body collection | 504 error envelope. |
| Fast batch (reserved) | 1 to 100 items | Unique item_id values required; invalid batch shape returns 422 when execution is available. |
| Complete batch (reserved) | 1 to 25 items | One configuration version applies to every item. |
| Model-call concurrency | 8 | 503 capacity with Retry-After: 2 when no slot is available. |
| Embedding concurrency | 32 | 503 capacity with Retry-After: 2 when no slot is available. |
| Batch concurrency (reserved) | 4, separate from single scans | 503 capacity on concurrency admission; rate-budget exhaustion is 429. |
| Reference readiness | At least 1,000 matching-revision points per organization | 503 reference_not_ready until seeded and ready; other ready organizations remain available. |
| Idempotency key | 1 to 256 characters | Required on prompt, document, and batch scans; reuse only for the same operation and body. |
| Usage query interval | At most 90 days; default preceding 7 days | RFC 3339 from inclusive, to exclusive; invalid, empty, or reversed intervals return 422. |
| Result retention | 30 days by default | 410 result_expired after expiry; physical cleanup is not implemented in this checkout. |
| Input retention | 0 days | Input text is not retained; opt-in is currently rejected. |
| Idempotency retention | Result lifetime plus 1 day | Expired results return 410 under the original key, never silent recomputation. |
| Confidence | Fast up to 0.99; T2 agreement up to 0.997; unknown is null | Heuristic support for the verdict, not a calibrated probability. |
The accepted document format list follows Bedrock's
document format contract.
AWS describes its per-document limit as
4.5 MB;
Zaun's exact byte cap is listed above. A provider can still reject an unreadable
or unsupported document with document_unreadable.
Rate and admission budgets
All applicable budgets must admit a request. Extra keys do not increase project or organization allowance. These are defaults, not a promise that a deployment has higher capacity available.
| Layer or scope | Fast | Complete | Document | Details |
|---|---|---|---|---|
| Key | 50 requests/s | 5 requests/s | 1 request/s | Per immutable key ID. |
| Project | 50 requests/s | 5 requests/s | 1 request/s | Independent of key count; published limits may be stricter. |
| Organization | 50 requests/s | 5 requests/s | 1 request/s | Independent of key and project count. |
| Local fallback during rate-store outage | 25 requests/s | 2.5 requests/s | 0.5 requests/s | Half-rate default, per process; never unlimited. |
| Operator management (reserved) | 2 requests/s | 2 requests/s | 2 requests/s | Separate administrative budget. |
Batch items count against their fast or complete class, with an additional
batch-item budget defaulting to 50 items/s at each scope. Concurrency admission
is separate from rate admission: 503 capacity means no execution slot;
429 rate_limited means an allowance is exhausted. Every retry, including
idempotent replay, consumes admission allowance. Malformed requests can also
consume admission allowance.
| Ingress limit | Default | Behavior |
|---|---|---|
| WAF per source IP | 2,000 requests per 5 minutes on /v1/ |
Requests above the budget are blocked before API authentication. |
| nginx per source IP | 50 requests/s, burst 100 without delay | Excess traffic is rejected before scan execution. |
| nginx connections per source IP | 50 | Excess connections are rejected. |
| nginx body cap | 5 MiB | Oversized bodies are rejected; JSON still has the stricter API cap. |
| nginx body timeout | 30 seconds | Stalled body uploads are terminated. |
Ingress values are the specified production defaults; they are not provided by development Compose. Upstream WAF or nginx denials may not use the API's JSON error envelope. Clients sharing an outbound IP also share its ingress budget.
Successful API admissions and 429 responses include X-RateLimit-Remaining,
the minimum remaining whole-token allowance across applicable budgets.
Retry-After on 429 gives the wait in whole seconds, rounded up. API 503
responses also include it; capacity denials use 2 seconds. Wait at least that
long before retrying with backoff. See key handling.
Error catalogue
API errors use this envelope. error.request_uuid is also returned as
X-Request-Id; retain it when contacting support. A correlation ID does not
promise a retrievable result if persistence failed.
{
"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 | Malformed JSON, invalid or empty input, unknown fields, missing scan type, invalid metadata, or duplicate batch IDs. Correct the request before retrying. |
unsupported_content |
422 | A message contains a non-text part. Send text or upload a supported document; remote resources are not fetched. |
input_too_long |
422 | Full-input token cap exceeded. Split the input; embedding oversize alone does not cause this error. |
idempotency_conflict |
409 | The key is bound to a different canonical body. Restore the original body or use a new key for new work. |
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. Its old idempotency key will not rescan; use a new key only if you intend a new assessment. |
capacity |
503 | Execution slots are exhausted or the model provider is throttling. Honor Retry-After: 2 and retry with backoff. |
model_unavailable |
503 | The configured model or access to it is unavailable. Retry with backoff; contact your operator if it persists. T2 execution failures can instead yield an inconclusive result. |
embedder_unavailable |
503 | The embedding service is unavailable. Honor Retry-After and retry with backoff. |
vector_search_unavailable |
503 | Reference search is unavailable. Honor Retry-After and retry with backoff; this is not an unknown verdict. |
embedding_invalid |
502 | The embedding service returned an invalid vector. Contact support with the request ID; do not interpret it as benign. |
result_not_persisted |
500 | The result and usage could not be saved. No retrievable result is promised; retry with the original idempotency key and contact support if persistent. |
document_unreadable |
422 | The document is encrypted, malformed, or rejected by the provider. Submit a readable supported variant. |
no_text_extracted |
422 | Neither text nor a document verdict was extracted. 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. |
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 | Key, project, organization, or batch rate budget exhausted. Honor Retry-After and inspect X-RateLimit-Remaining. |
operation_not_implemented |
501 | This build reserves the operation. Batch, proxy, and management are currently unavailable; 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 | The organization's reference set or revision is not ready. Ask the operator to seed and verify it; changing keys will not help. |
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. |
precondition_failed |
412 | Published configuration changed. The operator must read its current version and retry publication with the matching If-Match. |
method_not_allowed |
405 | The path exists but does not accept this method. Use the method documented in OpenAPI. |
This table includes every public code from the binary's error catalogue.
The internal model-throttling error shares public code capacity and status 503.
A failed complete classifier can instead return HTTP 200 with
status: inconclusive, verdict: unknown, and the cause in stages.t2.
Always inspect the result body, not just HTTP status.
Results, usage, and retry boundaries
Use a unique Idempotency-Key for each new scan. The same key and body replay the
original result within the same project and operation; a lost response is not
permission to reuse the key for different content. Deletion leaves a retry-window
tombstone so replay cannot recreate the deleted result. GET after deletion is
404; repeated own-project DELETE is 204. Cross-project read/delete is 404.
GET /v1/usage requires usage:read. group_by is day (default, UTC), model,
or stage. A missing or foreign-project key_id filter returns 404. An interval
with no usage returns an empty array and zero totals; a zero-length interval is
invalid. Revoked and expired keys retain historical usage. Deleting a result does
not erase usage. See data handling for retention boundaries.
Keys
Use a project key over HTTPS:
Authorization: Bearer zaun_scan_<64 hexadecimal characters>
Your Zaun operator delivers the secret once. Store it in your application's secret store and load it at runtime. Keep it out of URLs, source control, logs, and browser code. Only a hash and key lifecycle metadata are stored by Zaun; the plaintext cannot be recovered. Keys can expire or be revoked.
Each key belongs to one project and organization. The request cannot change that scope. Same-project keys with the appropriate permissions can access retained results and historical usage created by earlier keys, including after rotation. Other projects' results and usage are not accessible.
Scopes
| Scope | Operations |
|---|---|
scan:write |
Prompt and document scans; admission to reserved batch and proxy routes. |
result:read |
GET /v1/result/{uuid} for retained project results. |
result:delete |
DELETE /v1/result/{uuid} for retained project results. |
usage:read |
GET /v1/usage, optionally filtered to a key in the same project. |
Ask for only the scopes your client needs. A read-only key cannot scan. Public health and documentation routes need no credential. Customer keys cannot administer organizations, projects, policy, or other keys.
Rotate a key
- Ask your Zaun operator for a replacement with the required scopes and expiry.
- Store the replacement and switch every client to it. Both keys work during this overlap, provided neither has expired or been revoked.
- Verify a scan or permitted read with the replacement, then ask the operator to revoke the old key ID. A display prefix alone is not a unique ID.
- Remove the old secret from client configuration. Existing project results and historical usage remain available to appropriately scoped replacements.
If a secret is exposed, ask for immediate revocation. Revocation and expiry reject subsequent authorization checks; work already admitted can finish. Restarting the service or losing the rate-limit store cannot reactivate a key. Creating replacements does not increase the project or organization rate budget.
Authentication and readiness errors
| Response | Meaning | What to do |
|---|---|---|
401 unauthorized |
Missing, malformed, unknown, expired, or revoked bearer. Disabled projects or organizations cannot admit new scans. An operator bearer is not a project key. | Check the deployed secret, expiry, and project status with your operator. |
403 forbidden |
A recognized credential lacks the route's scope or is a customer key attempting administration. | Use a key with the required scope; ask your operator to perform administrative actions. |
503 reference_not_ready |
The organization's reference set or configured revision is not ready to scan. | Ask your operator to seed and verify the organization. Rotating the key does not fix this. |
503 database_unavailable |
Authentication or result storage is temporarily unavailable. | Honor Retry-After and retry with backoff. Authorization is not granted from stale cached state. |
Disabled projects and organizations still allow otherwise active scoped keys
to read and delete retained results until expiry. An unknown scan verdict is
different from an authentication or readiness error; see concepts.
Rate limits and retries
X-RateLimit-Remaining reports the minimum remaining whole-token allowance
across the applicable key, project, and organization budgets. It appears on
successful admissions and rate-limit denials. HTTP 429 rate_limited includes
Retry-After, rounded up to whole seconds. Wait at least that long, then retry
with backoff. HTTP 503 also includes Retry-After; capacity denials use 2 seconds.
Send a unique Idempotency-Key for each new scan. Reuse it with the same body
when retrying after a lost response. Every retry, including replay, consumes
admission allowance. Do not rotate credentials or create extra keys to bypass
tenant limits. See limits and errors for budgets and retry outcomes.
Operator boundary
Onboarding and key delivery are operator-managed; there is no customer
self-service key UI. The management routes described in OpenAPI are reserved
and return 501 operation_not_implemented in this checkout. Coordinate key
changes with the operator instead of calling them from your application.
The management contract uses a separate admin credential for organization and
project setup, policy publication, and key creation/listing/revocation. It does
not authorize public scans. Key creation discloses a secret once with
Cache-Control: no-store. A committed retry returns 409 key_already_created
with metadata, never another secret. If the creation response was lost, the
operator revokes the issued ID and creates a replacement under a new idempotency
key. Configuration publication uses If-Match; a stale version returns 412.
Key listings and audit records do not expose secrets or hashes.
Data handling
Scan content is processed to produce an assessment. Raw prompt text and extracted
document text are not retained by default (retain_input_days: 0) and never
appear in returned results. This checkout rejects input-retention opt-in until
its retention cleanup is available.
What is stored
Stored results include verdicts, findings with locations rather than quoted text, input hashes, caller-supplied metadata, session correlation IDs, observed client IP, configuration and revision identifiers, timestamps, and model usage. Results, usage, and idempotency records belong to the key's project. Metadata is caller-asserted, not proof of identity; it is never sent to the embedder or classifier. Keep secrets and raw input out of metadata because it is stored.
Customer key secrets are disclosed once; Zaun stores only their hashes and lifecycle metadata. Operational logs contain request IDs, status, counts, and timing, not raw input, document bytes, metadata, secrets, or key hashes.
Where content goes
Fast prompt scans send selected text to Zaun's private embedding service.
Complete scans also send the full selected text and project context to Amazon
Bedrock. Document bytes go to Bedrock in us-east-1 for extraction and
assessment, even on fast scans. Extracted chunks then follow the scan path.
Document bytes stay in memory rather than being written to local disk. Scans
do not fetch remote URLs or add customer content to the curated reference set.
The production contract requires that submitted content is not retained by the provider. Bedrock does not give model providers access to prompts or outputs. AWS retention can depend on the selected model and account settings; the no-retention production configuration must be verified when those change. This checkout does not attest to live AWS settings. See AWS's data protection and data retention documentation.
The embedding representation has a 2,000-character head/tail elision limit.
Messages that would be shortened are instead marked bypassed_oversize, as are
messages over the embedding token window. Complete scans can still send the
full selected text to Bedrock within the full-input cap. Elision therefore is
not a privacy filter. Document page coverage is model-reported; truncation
produces an inconclusive assessment rather than a claim of complete inspection.
Retention and deletion
Results expire after 30 days by default. Idempotency records outlive them by one day; an expired replay returns 410 instead of silently rescanning. Expiry prevents API access, but automated physical retention cleanup is not implemented in this checkout, so TTL is not a verified physical purge deadline.
DELETE /v1/result/{uuid} removes the result content, any retained input, and
the replay link. A minimal tombstone remains during the retry window to prevent
recreation by late work. Repeated deletion returns 204; reads after deletion
return 404. Deletion cannot undo an already running provider request.
Usage accounting, key lifecycle records, and operator audit log entries survive result deletion. Backup copies survive until backup expiry; the specified production backup window is seven days. Usage and audit retention periods are not defined in this checkout. Ask your operator for the deployed retention policy when those periods matter to your application.