Docs · API reference
Docs · API reference
The API, exactly as built.
Authentication, roles, errors, conventions, the objects you get back, and every endpoint. This page is written from the API code.
Base URL and versions
Production: https://api.attestiq.ai (planned; pilot clients get the current URL from us). Every route is under /v1. Modules mount under /v1/<module>, such as /v1/validate.
Rules and logic are versioned separately from the URL. Each validation records its pinned policy pack (disputes-us@0.2.0), and each evidence record lists the engine, check, and pack versions that produced it.
Note
The OpenAPI specification is provided on request. The API server's interactive docs are not exposed to external clients.
Authentication
Send your key in X-API-Key, or as Authorization: Bearer <key>. Keys look like atq_live_…. Missing or unknown keys get 401 unauthenticated.
Every module route checks two things: the key's role allows the action (else 403 forbidden), and your tenant's contract includes the module (else 402 module_not_entitled).
Every resource belongs to one tenant. Another tenant's id returns 404, never 403, so existence does not leak.
curl https://api.attestiq.ai/v1/me -H "X-API-Key: atq_live_…"
# or
curl https://api.attestiq.ai/v1/me -H "Authorization: Bearer atq_live_…"Roles and permissions
| Role | Permissions |
|---|---|
| service | validate:submit, validate:read, authorize:read, policy:read, evidence:read |
| viewer | read* |
| engineer | read*, validate:submit, validate:eval, webhooks:manage |
| compliance_analyst | read*, validate:review, audit:read |
| compliance_officer | read*, validate:review, validate:eval, audit:read, reports:attest |
| model_risk | read*, validate:eval, audit:read |
| payments_lead | read*, authorize:write |
| admin | every permission, including tenant:admin, keys:manage, webhooks:manage |
read* is validate:read, authorize:read, reports:read, evidence:read, policy:read, tenant:read. GET /v1/me lists your key's exact permissions.
Errors
Errors return a JSON body with a machine-readable code and a human message.
Every response carries an X-Request-ID header. Send your own X-Request-ID and it is echoed back. Include it when you contact us about a request.
// Every error, every status
{"code": "not_found", "message": "validation not found"}
{"code": "unauthenticated", "message": "invalid API key"}
{"code": "forbidden", "message": "requires validate:review"}
// Request validation (422) adds up to 10 field errors
{"code": "invalid_request", "message": "request validation failed",
"errors": [{"loc": ["body", "agent"], "msg": "Field required"},
{"loc": ["body", "output"], "msg": "Field required"}]}def error_code(body: dict) -> str:
return body.get("code", "error")| Status | code | When |
|---|---|---|
| 401 | unauthenticated | Missing or invalid key. |
| 402 | module_not_entitled | The module is gated or not on your contract. |
| 403 | forbidden | The key's role lacks the permission. The message names it. |
| 404 | not_found | Unknown id, or another tenant's. |
| 409 | conflict | Idempotency key reused with a different body; a decision on a validation that is not awaiting review; releasing a fail before its critical findings are labeled. |
| 422 | invalid_request | Bad body or query, missing required record types, unknown workload, output too long, unknown webhook event, unsafe webhook URL. |
| 422 | pci_data_rejected | A full card number in the records. |
| 429 | quota_exceeded | The monthly pilot quota for the module is used up. |
| 429 | rate_limited | Public sandbox only: too many requests from one address in a minute. |
Conventions
- IDs are prefixed strings:
val_,fnd_,evl_,whe_,whd_; evidence refs look likeEV-2026-…. - Money is integer cents (
amount_cents: 24900). Timestamps are ISO-8601 UTC. Dates areYYYY-MM-DD. - Idempotency:
idempotency_keyin the body ofPOST /v1/validate/validations. See the quickstart.
Pagination and filters
GET /v1/validate/validations returns newest first. limit defaults to 100, maximum 500. Pass the response's next_before as before for the next page; it is null on the last page. Filters: verdict, review_status, workload, and purpose (default production; eval for eval runs).
GET /v1/audit works the same way with before_seq and next_before_seq, plus module and action (prefix match). Other lists return a fixed number of recent items, noted in the endpoint table.
# Newest first. Pass the last id you saw as "before".
curl "https://api.attestiq.ai/v1/validate/validations?verdict=hold&review_status=pending&limit=100" \
-H "X-API-Key: $ATTESTIQ_API_KEY"
{"object": "list", "data": [ ... 100 validations ... ],
"next_before": "val_01a0b7f8cd7cd4e8b76923ba"} # null on the last page
curl "https://api.attestiq.ai/v1/validate/validations?verdict=hold&review_status=pending&limit=100&before=val_01a0b7f8cd7cd4e8b76923ba" \
-H "X-API-Key: $ATTESTIQ_API_KEY"Limits
| Limit | Value |
|---|---|
| Output size | 60,000 characters (default) |
| Records per validation | 50 |
| Cases per eval upload | 500 |
| List page size | 500 |
| Monthly volume | Your pilot quota per module; 429 quota_exceeded when used up. GET /v1/me shows quota and used_this_month. |
| Public sandbox | Per-IP limit, 20 requests a minute by default; 8,000-character output |
There is no per-key request rate limit on the authenticated API today.
Objects
Validation
| Field | Description |
|---|---|
| id, object | val_…, "validation" |
| workload, policy_pack | The workload and the pinned pack version |
| agent, external_id | {id, version, model} as sent; your case id |
| status | queued | running | completed | failed |
| verdict, score | pass | pass_with_warnings | hold | fail; 0–100. Null until completed |
| counts | {total, binding, advisory, by_severity, by_code} |
| review_status | not_required | pending | released | blocked | returned |
| summary, summary_source | Plain-language summary; template or model |
| output_hash | sha256: of the draft (see the data page for async mode) |
| verdict_ref, evidence_refs | The verdict's evidence record, and every evidence record for the run |
| usage | Steps, model calls, tokens, latency, model mode |
| error | Set when status is failed |
| submitted_by, created_at, completed_at | Actor and timestamps |
| reviewed_by, reviewed_at, review_rationale | Set by a review decision |
| storage_mode, content_purged_at | redacted | minimal; when stored content was purged |
| findings | Array of Finding (on create, decision, and detail) |
| output_redacted, output_format, records, evidence, trace | Detail only (GET /validations/{id}) |
Finding
| Field | Description |
|---|---|
| id | fnd_… |
| code, family | e.g. FACT.AMOUNT_MISMATCH, fact. GET /v1/validate/taxonomy lists every code |
| severity | low | medium | high | critical |
| validator, assurance | Which check; deterministic | model_extracted | model_judged | advisory |
| message, quote | What is wrong; the sentence in the draft (redacted; empty in minimal mode) |
| expected, observed | The record's value and the draft's value |
| record_ref | Where the expected value lives, e.g. transaction:TXN-88213#amount_cents |
| rule_id, citation | The pack rule and {pack, rule_id, cite, source, url, review_status} |
| confidence, related | 1.0 for deterministic checks; findings folded into this one |
| evidence_ref | The finding's signed evidence record |
| label, label_note, labeled_by | Reviewer label: confirmed | false_positive |
Evidence record
hash is SHA-256 over the canonical JSON of the record. kind is input, claims, finding, or verdict for Validate. Recompute one with GET /v1/evidence/{ref}/verify, or all of a validation's with GET /v1/validate/validations/{id}/verify.
// GET /v1/evidence/EV-2026-1M2VZHKBZWN8S6
{
"ref": "EV-2026-1M2VZHKBZWN8S6",
"kind": "verdict",
"subject": "hold (60)",
"producer": "validate.reconcile",
"body": {
"detail": "Verdict hold, score 60, 2 binding finding(s).",
"verdict": "hold", "score": 60, "counts": {...},
"requires_review": true,
"finding_refs": ["EV-2026-1M2VZHKBYNYCTZ", "EV-2026-1M2VZHKBYRGPYK"],
"hitl_threshold": "high",
"versions": {"engine": "validate.engine/0.1.0", "checks": "validate.checks/0.2.0",
"pack": "disputes-us@0.2.0#05985a5aec71", ...}
},
"sources": ["EV-2026-1M2VZHKBWYQD81", "EV-2026-1M2VZHKBXJKDCB", ...],
"ruleset_version": "validate.reconcile/0.1.0",
"input_hash": "sha256:6854dd6d…",
"hash": "sha256:40021523…",
"module": "validate",
"run_id": "val_01a0b7f8cd7cd4e8b76923ba",
"store_uri": "…",
"created_at": "2026-09-18T16:42:11Z",
"verified": true
}
// GET /v1/validate/validations/{id}/verify
{"run_id": "val_01a0b7f8cd7cd4e8b76923ba", "checked": 5, "ok": true,
"records": [{"ref": "EV-2026-1M2VZHKBWYQD81", "ok": true}, ...]}Endpoints
Every route in the API. Paths in each group are relative to the group's prefix. Module routes also require the module on your contract.
Validate · /v1/validate
| Endpoint | Permission | Purpose |
|---|---|---|
| POST/validations | validate:submit | Validate an output. Sync: 201 with the verdict. Async: 202, verdict by webhook. Replay: 200 with Idempotent-Replayed: true. |
| GET/validations | validate:read | List, newest first. Filters: verdict, review_status, workload, purpose, limit, before. |
| GET/validations/{id} | validate:read | Detail: findings, redacted output and records, evidence records, trace. |
| GET/validations/{id}/verify | validate:read | Recompute every evidence hash for the validation. |
| POST/validations/{id}/findings/{finding_id}/label | validate:review | Label a finding: confirmed or false_positive, with an optional note. |
| POST/validations/{id}/findings | validate:review | Add a finding the validator missed (code, severity, message, quote). |
| POST/validations/{id}/decision | validate:review | release, block, or return_to_agent, with a rationale of at least 10 characters. Only while review_status is pending. |
| DELETE/validations/{id}/content | tenant:admin | Purge stored text, records, and quotes now. Returns {id, content_purged_at}. |
| GET/stats | validate:read | Counts by verdict, pending reviews, top codes, labels, agents. |
| GET/taxonomy | validate:read | Error codes, assurance levels, and workloads with their policy packs. |
| GET/samples | validate:read | Synthetic sample drafts and records. |
| POST/evals | validate:eval | Run an evaluation: up to 500 cases (labeled with expected_codes, or not), or dataset finval-seed. |
| GET/evals | validate:read | The 50 most recent eval runs. |
| GET/evals/{id} | validate:read | One eval run with its report and per-case results. |
Platform · /v1
| Endpoint | Permission | Purpose |
|---|---|---|
| GET/me | any key | The key's role, permissions, tenant, entitlements, and model mode. |
| GET/modules | any key | Modules, their status, and whether this tenant is entitled. |
| GET/policy-packs | policy:read | Policy packs in force. |
| GET/policy-packs/{ref} | policy:read | One pack with every rule, citation, and review status. |
| GET/evidence/{ref} | evidence:read | One evidence record, with verified: true | false. |
| GET/evidence/{ref}/verify | evidence:read | Recompute one evidence hash: {ref, ok, hash}. |
| GET/audit | audit:read | Audit events, newest first. Filters: limit (≤500), before_seq, module, action (prefix). |
| GET/audit/verify | audit:read | Recompute the tenant's audit hash chain: {ok, checked, first_bad_seq, head_hash}. |
| POST/reports/attestation | reports:attest | Generate the attestation pack for period_start..period_end (default: last 90 days). |
| GET/reports | reports:read | The 50 most recent reports. |
| GET/reports/{id} | reports:read | One report with its Markdown content. |
| POST/webhooks | webhooks:manage | Register an endpoint. The secret is returned once. |
| GET/webhooks | webhooks:manage | Endpoints and the 50 most recent deliveries. |
| GET/settings/data | tenant:read | Storage mode, retention days, last sweep time. |
| PUT/settings/data | tenant:admin | Change storage_mode and/or retention_days (7–3650). |
| POST/settings/data/sweep | tenant:admin | Purge content past retention now: {purged, by_module, cutoff}. |
| GET/jobs | tenant:admin | The 100 most recent background jobs. |
Authorize · /v1/authorize
Authorize is in preview; these routes return 402 module_not_entitled unless your contract includes it.
| Endpoint | Permission | Purpose |
|---|---|---|
| GET/overview | authorize:read | Portfolio approval rates, trend, merchants, top leaks. |
| GET/merchants/{id} | authorize:read | Merchant trend, decline drivers, recommendations. |
| POST/diagnoses | authorize:write | Diagnose all merchants or one; each recommendation is validated. |
| GET/recommendations | authorize:read | Recommendations; filters merchant_id, status. |
| PATCH/recommendations/{id} | authorize:write | Move through reviewed → applied → measured, or rejected. |
| POST/ask | authorize:read | A plain-language question about approval rates. |
| POST/transactions/batch | authorize:write | Ingest authorization attempts. BIN only; any card number rejects the batch. |
Public sandbox · /v1/demo
No key. Synthetic data only. Rate limited per IP address.
| Endpoint | Permission | Purpose |
|---|---|---|
| GET/samples | none | Synthetic samples. |
| POST/validate | none | {sample_id, output?}: run Validate in memory. Nothing is stored. |
| POST/ask | none | The home-page question box, on the synthetic demo tenant. |
Operations
| Endpoint | Permission | Purpose |
|---|---|---|
| GET/health | none | {status, version}. |
| GET/ready | none | Database reachable; model mode; environment. |
Webhooks
Body: {id, type, created, data}, serialized as compact JSON with sorted keys. For validation events, data is {validation_id, external_id, workload, agent_id, verdict, score, review_status, verdict_ref, findings: [{code, severity, message, evidence_ref}]}.
Headers: Attestiq-Event, Attestiq-Delivery, Attestiq-Signature: t=<unix>,v1=<hex>. The signature is HMAC-SHA256 with your endpoint secret over "<t>." followed by the raw body.
Delivery: HTTPS only; 5-second timeout; any non-2xx is retried with exponential backoff, up to 8 attempts. Deliveries come from a transactional outbox, so an event is recorded in the same transaction as the change it announces. Verification code is in the quickstart.
POST /attestiq HTTP/1.1
Content-Type: application/json
Attestiq-Event: validation.held
Attestiq-Delivery: whd_01a0b7f8cda1c2e3d4f5a6b7
Attestiq-Signature: t=1789749731,v1=5d1c0e…
{"created":"2026-09-18T16:42:11Z","data":{"agent_id":"chargeback-responder","external_id":"CB-77120",
"findings":[{"code":"POLICY.MISSING_REQUIRED_EVIDENCE","evidence_ref":"EV-2026-1M2VZHKBYNYCTZ",
"message":"No proof of delivery or of services rendered for a 'not received' dispute.","severity":"high"},
{"code":"POLICY.TIMELINE_VIOLATION","evidence_ref":"EV-2026-1M2VZHKBYRGPYK","message":"Deadline missed by 7 day(s): ...",
"severity":"high"}],"review_status":"pending","score":60,"validation_id":"val_01a0b7f8cd7cd4e8b76923ba",
"verdict":"hold","verdict_ref":"EV-2026-1M2VZHKBZWN8S6","workload":"disputes.representment"},
"id":"whd_01a0b7f8cda1c2e3d4f5a6b7","type":"validation.held"}MCP tools
attestiq-mcp is a stdio MCP server and a thin client of this API. Give it a service key; it can submit and read, never review or release. It reads ATTESTIQ_API_URL and ATTESTIQ_API_KEY.
| Tool | Arguments | Returns |
|---|---|---|
| validate_output | workload, output, records_json (a JSON array string), agent_id = "mcp-agent", external_id | {validation_id, verdict, score, review_status, summary, must_fix[], verdict_ref} |
| get_validation | validation_id | The same compact shape |
| list_workloads | — | {workloads, codes} |
| diagnose_authorization | question | Authorize's answer (preview; needs the module) |
The Python SDK (attestiq-sdk 0.1.0) and the MCP server are provided to pilot clients. The SDK covers validations.create / get / list / wait, evals.run, me(), and verify_webhook; errors raise AttestiqError with status, code, and message.
{
"mcpServers": {
"attestiq": {
"command": "attestiq-mcp",
"env": {
"ATTESTIQ_API_URL": "https://api.attestiq.ai",
"ATTESTIQ_API_KEY": "atq_live_…"
}
}
}
}