Attestiq
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.

Authentication
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

RolePermissions
servicevalidate:submit, validate:read, authorize:read, policy:read, evidence:read
viewerread*
engineerread*, validate:submit, validate:eval, webhooks:manage
compliance_analystread*, validate:review, audit:read
compliance_officerread*, validate:review, validate:eval, audit:read, reports:attest
model_riskread*, validate:eval, audit:read
payments_leadread*, authorize:write
adminevery 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.

Error bodies
// 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"}]}
Read the code
def error_code(body: dict) -> str:
    return body.get("code", "error")
StatuscodeWhen
401unauthenticatedMissing or invalid key.
402module_not_entitledThe module is gated or not on your contract.
403forbiddenThe key's role lacks the permission. The message names it.
404not_foundUnknown id, or another tenant's.
409conflictIdempotency 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.
422invalid_requestBad body or query, missing required record types, unknown workload, output too long, unknown webhook event, unsafe webhook URL.
422pci_data_rejectedA full card number in the records.
429quota_exceededThe monthly pilot quota for the module is used up.
429rate_limitedPublic sandbox only: too many requests from one address in a minute.

Conventions

  • IDs are prefixed strings: val_, fnd_, evl_, whe_, whd_; evidence refs look like EV-2026-….
  • Money is integer cents (amount_cents: 24900). Timestamps are ISO-8601 UTC. Dates are YYYY-MM-DD.
  • Idempotency: idempotency_key in the body of POST /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.

Cursor pagination
# 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

LimitValue
Output size60,000 characters (default)
Records per validation50
Cases per eval upload500
List page size500
Monthly volumeYour pilot quota per module; 429 quota_exceeded when used up. GET /v1/me shows quota and used_this_month.
Public sandboxPer-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

FieldDescription
id, objectval_…, "validation"
workload, policy_packThe workload and the pinned pack version
agent, external_id{id, version, model} as sent; your case id
statusqueued | running | completed | failed
verdict, scorepass | pass_with_warnings | hold | fail; 0–100. Null until completed
counts{total, binding, advisory, by_severity, by_code}
review_statusnot_required | pending | released | blocked | returned
summary, summary_sourcePlain-language summary; template or model
output_hashsha256: of the draft (see the data page for async mode)
verdict_ref, evidence_refsThe verdict's evidence record, and every evidence record for the run
usageSteps, model calls, tokens, latency, model mode
errorSet when status is failed
submitted_by, created_at, completed_atActor and timestamps
reviewed_by, reviewed_at, review_rationaleSet by a review decision
storage_mode, content_purged_atredacted | minimal; when stored content was purged
findingsArray of Finding (on create, decision, and detail)
output_redacted, output_format, records, evidence, traceDetail only (GET /validations/{id})

Finding

FieldDescription
idfnd_…
code, familye.g. FACT.AMOUNT_MISMATCH, fact. GET /v1/validate/taxonomy lists every code
severitylow | medium | high | critical
validator, assuranceWhich check; deterministic | model_extracted | model_judged | advisory
message, quoteWhat is wrong; the sentence in the draft (redacted; empty in minimal mode)
expected, observedThe record's value and the draft's value
record_refWhere the expected value lives, e.g. transaction:TXN-88213#amount_cents
rule_id, citationThe pack rule and {pack, rule_id, cite, source, url, review_status}
confidence, related1.0 for deterministic checks; findings folded into this one
evidence_refThe finding's signed evidence record
label, label_note, labeled_byReviewer 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.

Evidence record and verification
// 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

EndpointPermissionPurpose
POST/validationsvalidate:submitValidate an output. Sync: 201 with the verdict. Async: 202, verdict by webhook. Replay: 200 with Idempotent-Replayed: true.
GET/validationsvalidate:readList, newest first. Filters: verdict, review_status, workload, purpose, limit, before.
GET/validations/{id}validate:readDetail: findings, redacted output and records, evidence records, trace.
GET/validations/{id}/verifyvalidate:readRecompute every evidence hash for the validation.
POST/validations/{id}/findings/{finding_id}/labelvalidate:reviewLabel a finding: confirmed or false_positive, with an optional note.
POST/validations/{id}/findingsvalidate:reviewAdd a finding the validator missed (code, severity, message, quote).
POST/validations/{id}/decisionvalidate:reviewrelease, block, or return_to_agent, with a rationale of at least 10 characters. Only while review_status is pending.
DELETE/validations/{id}/contenttenant:adminPurge stored text, records, and quotes now. Returns {id, content_purged_at}.
GET/statsvalidate:readCounts by verdict, pending reviews, top codes, labels, agents.
GET/taxonomyvalidate:readError codes, assurance levels, and workloads with their policy packs.
GET/samplesvalidate:readSynthetic sample drafts and records.
POST/evalsvalidate:evalRun an evaluation: up to 500 cases (labeled with expected_codes, or not), or dataset finval-seed.
GET/evalsvalidate:readThe 50 most recent eval runs.
GET/evals/{id}validate:readOne eval run with its report and per-case results.

Platform · /v1

EndpointPermissionPurpose
GET/meany keyThe key's role, permissions, tenant, entitlements, and model mode.
GET/modulesany keyModules, their status, and whether this tenant is entitled.
GET/policy-packspolicy:readPolicy packs in force.
GET/policy-packs/{ref}policy:readOne pack with every rule, citation, and review status.
GET/evidence/{ref}evidence:readOne evidence record, with verified: true | false.
GET/evidence/{ref}/verifyevidence:readRecompute one evidence hash: {ref, ok, hash}.
GET/auditaudit:readAudit events, newest first. Filters: limit (≤500), before_seq, module, action (prefix).
GET/audit/verifyaudit:readRecompute the tenant's audit hash chain: {ok, checked, first_bad_seq, head_hash}.
POST/reports/attestationreports:attestGenerate the attestation pack for period_start..period_end (default: last 90 days).
GET/reportsreports:readThe 50 most recent reports.
GET/reports/{id}reports:readOne report with its Markdown content.
POST/webhookswebhooks:manageRegister an endpoint. The secret is returned once.
GET/webhookswebhooks:manageEndpoints and the 50 most recent deliveries.
GET/settings/datatenant:readStorage mode, retention days, last sweep time.
PUT/settings/datatenant:adminChange storage_mode and/or retention_days (7–3650).
POST/settings/data/sweeptenant:adminPurge content past retention now: {purged, by_module, cutoff}.
GET/jobstenant:adminThe 100 most recent background jobs.

Authorize · /v1/authorize

Authorize is in preview; these routes return 402 module_not_entitled unless your contract includes it.

EndpointPermissionPurpose
GET/overviewauthorize:readPortfolio approval rates, trend, merchants, top leaks.
GET/merchants/{id}authorize:readMerchant trend, decline drivers, recommendations.
POST/diagnosesauthorize:writeDiagnose all merchants or one; each recommendation is validated.
GET/recommendationsauthorize:readRecommendations; filters merchant_id, status.
PATCH/recommendations/{id}authorize:writeMove through reviewed → applied → measured, or rejected.
POST/askauthorize:readA plain-language question about approval rates.
POST/transactions/batchauthorize:writeIngest authorization attempts. BIN only; any card number rejects the batch.

Public sandbox · /v1/demo

No key. Synthetic data only. Rate limited per IP address.

EndpointPermissionPurpose
GET/samplesnoneSynthetic samples.
POST/validatenone{sample_id, output?}: run Validate in memory. Nothing is stored.
POST/asknoneThe home-page question box, on the synthetic demo tenant.

Operations

EndpointPermissionPurpose
GET/healthnone{status, version}.
GET/readynoneDatabase 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.

Delivery
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.

ToolArgumentsReturns
validate_outputworkload, 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_validationvalidation_idThe same compact shape
list_workloads—{workloads, codes}
diagnose_authorizationquestionAuthorize'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.

Client config
{
  "mcpServers": {
    "attestiq": {
      "command": "attestiq-mcp",
      "env": {
        "ATTESTIQ_API_URL": "https://api.attestiq.ai",
        "ATTESTIQ_API_KEY": "atq_live_…"
      }
    }
  }
}