Docs · Quickstart
Docs · Quickstart
From key to verdict, step by step.
Everything an engineering team needs to connect an agent to Attestiq: what to prepare, how keys work, the first call, the four verdicts, async delivery, and what to check before production.
Step 1
What you need
- An API key from us. Access is by pilot invitation. We issue a service key for your agent and user keys for the people on your team.
- Your agent, and a place to call us from. The call goes right after the agent drafts and before anything is sent. Any language that can make an HTTPS request works; there is a Python SDK and an MCP server.
- The minimum records per draft. For a chargeback response: the transaction, the chargeback, and your representment details (response date, evidence list). Never full card numbers. See the data contract.
- A decision for each verdict. What your workflow does on
holdandfail, and what it does if Attestiq is unreachable.
Before you have a key, the public sandbox runs the validator on synthetic samples. It needs no key, stores nothing, and is rate limited per IP address (20 requests a minute by default).
# Public sandbox: no key, synthetic samples only, nothing stored.
curl https://api.attestiq.ai/v1/demo/samples
curl https://api.attestiq.ai/v1/demo/validate -H "Content-Type: application/json" \
-d '{"sample_id": "representment-late"}'Step 2
API keys
Send the key in the X-API-Key header (or Authorization: Bearer <key>). Each key belongs to one tenant and has one role. We store only a SHA-256 hash of each key.
Keep the service key in your secret manager and out of the agent's prompt and tool arguments. The full role-to-permission table is in the reference.
Planned
SSO (OIDC/SAML) for people is planned. Keys remain for machines.
curl https://api.attestiq.ai/v1/me -H "X-API-Key: $ATTESTIQ_API_KEY"
{
"key_id": "key_…", "name": "dispute-drafter agent", "email": "",
"role": "service", "kind": "service",
"permissions": ["authorize:read", "evidence:read", "policy:read",
"validate:read", "validate:submit"],
"tenant": {"id": "…", "name": "…", "kind": "processor", "synthetic": true},
"entitlements": {"validate": {"module": "validate", "plan": "pilot", "status": "active",
"quota": 10000, "period": "month", "used_this_month": 26, ...}},
"model_gateway": {"provider": "none", "models": {}, "mode": "deterministic"},
"version": "0.1.0"
}| Key | Role | Can | Cannot |
|---|---|---|---|
| Service key (your agent, the MCP server) | service | Submit validations; read validations, evidence, policy packs | Review, release, label, run evals, manage webhooks, change settings |
| User keys (people) | compliance_analyst, compliance_officer, model_risk, engineer, admin, … | What the role allows: review, evals, webhooks, attestation, settings | Anything outside the role |
Step 3
Environments
There is one API. Your key decides which tenant you reach.
| Environment | What it is | Data |
|---|---|---|
| Public sandbox | /v1/demo/*, no key. Runs the validator in memory on built-in samples. | Synthetic only; nothing stored |
| Sandbox tenant | A tenant marked synthetic: tenant.synthetic is true in GET /v1/me. Pilots start here. | Synthetic or de-identified historical drafts |
| Production tenant | Your tenant for live drafts, under a signed DPA. | Your real records, minimum fields only |
Planned
The hosted environment at https://api.attestiq.ai is not live yet. Dedicated and in-your-AWS deployments are described under deployment options.
Step 4
Your first validation
Send one request per draft. The default is mode: "sync": the response is the finished validation, with a 201. With models off, the checks take milliseconds; enabling a model adds the model's latency.
With MCP
Agents that speak MCP can call Attestiq as a tool. Run attestiq-mcp (stdio) with a service key in its environment. The tool validate_output always validates in sync mode and returns a compact result: the verdict, the summary, and must_fix, the non-advisory findings.
curl https://api.attestiq.ai/v1/validate/validations \
-H "X-API-Key: $ATTESTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"workload": "disputes.representment",
"agent": {"id": "chargeback-responder", "version": "0.9.0"},
"output": "Second presentment — Mastercard chargeback CB-77120\n\nMerchant: Lumen Travel Gear. Transaction amount: $249.00 on July 2, 2026, card ending in 8820. ...",
"records": [
{"type": "transaction", "id": "TXN-51007", "data": {
"amount_cents": 24900, "currency": "USD", "transaction_date": "2026-07-02",
"merchant_name": "Lumen Travel Gear", "network": "mastercard",
"card_type": "credit", "card_last4": "8820"}},
{"type": "chargeback", "id": "CB-77120", "data": {
"network": "mastercard", "reason_code": "4853", "category": "not_received",
"chargeback_date": "2026-07-28", "amount_cents": 24900}},
{"type": "representment", "id": "REP-77120", "data": {
"response_date": "2026-09-18", "evidence": ["invoice"]}}
],
"external_id": "CB-77120",
"idempotency_key": "CB-77120-draft-1"
}
JSON| Field | Required | What to send |
|---|---|---|
| workload | yes | Selects the checks and the policy pack: disputes.representment, disputes.cardholder_letter, authorize.retry_policy, authorize.recommendation. GET /v1/validate/taxonomy lists them. |
| agent | yes | {id, version?, model?}. Used for per-agent reporting. |
| output | yes | The draft as text, or a JSON object for structured workloads. Up to 60,000 characters by default. |
| records | per workload | Up to 50 {type, id, data} records. A missing required type returns 422 naming it. |
| external_id | no | Your case or document id. Echoed in webhooks. |
| idempotency_key | recommended | One per draft. See step 8. |
| mode | no | sync (default) or async. |
| policy_pack | no | Pin a pack version, e.g. disputes-us@0.2.0. Default: the latest for the workload. |
| as_of | no | YYYY-MM-DD evaluation date when the draft carries none. |
Step 5
Read the verdict
| verdict | Meaning | review_status |
|---|---|---|
| pass | No finding at medium severity or above. | not_required |
| pass_with_warnings | Medium findings only. Releasable; review the notes. | not_required |
| hold | At least one high finding. A reviewer must decide. | pending |
| fail | At least one critical finding, such as invented evidence or a card number in the draft. | pending |
Each finding tells you what to fix:
code,severity,message: what is wrong, e.g.POLICY.TIMELINE_VIOLATION.quote: the sentence in the draft, when there is one. Missing-content findings have no quote.expected/observedandrecord_ref: the value in your record and where it is (chargeback:CB-77120#chargeback_date).rule_idandcitation: the rule and its source.citation.review_statussays whether an expert has reviewed that rule.assurance: how it was established —deterministic,model_extracted,model_judged, oradvisory(informational, never affects the verdict).evidence_ref: the signed evidence record. Storeidandverdict_refwith your case.
HTTP/1.1 201 Created
X-Request-ID: 3f9c1a7e2b6d4c80
{
"id": "val_01a0b7f8cd7cd4e8b76923ba",
"object": "validation",
"purpose": "production",
"workload": "disputes.representment",
"policy_pack": "disputes-us@0.2.0",
"agent": {"id": "chargeback-responder", "version": "0.9.0", "model": ""},
"external_id": "CB-77120",
"status": "completed",
"verdict": "hold",
"score": 60,
"counts": {"total": 2, "binding": 2, "advisory": 0,
"by_severity": {"high": 2},
"by_code": {"POLICY.MISSING_REQUIRED_EVIDENCE": 1, "POLICY.TIMELINE_VIOLATION": 1}},
"review_status": "pending",
"summary": "Hold — a reviewer must decide before this document is released. Score 60/100. ...",
"summary_source": "template",
"output_hash": "sha256:6f892b5aa2f165f537c46ad74ca8cd44a1a1f340d7dc3e0820faa4a8888d95a4",
"verdict_ref": "EV-2026-1M2VZHKBZWN8S6",
"evidence_refs": ["EV-2026-1M2VZHKBWYQD81", "EV-2026-1M2VZHKBXJKDCB", "EV-2026-1M2VZHKBYNYCTZ",
"EV-2026-1M2VZHKBYRGPYK", "EV-2026-1M2VZHKBZWN8S6"],
"usage": {"tokens_in": 0, "tokens_out": 0, "steps": 10, "latency_ms": 0, "llm_calls": 0,
"model_mode": "deterministic", "trace": {"ok": 7, "skipped": 3}},
"error": null,
"submitted_by": "service:key_demo_06",
"created_at": "2026-09-18T16:42:11Z",
"completed_at": "2026-09-18T16:42:11Z",
"reviewed_by": null,
"reviewed_at": null,
"review_rationale": null,
"storage_mode": "redacted",
"content_purged_at": null,
"findings": [
{ "id": "fnd_01a0b7f8cd803dbb97fe4400", "code": "POLICY.MISSING_REQUIRED_EVIDENCE", "...": "..." },
{
"id": "fnd_01a0b7f8cd8083258e369f36",
"code": "POLICY.TIMELINE_VIOLATION",
"family": "policy",
"severity": "high",
"validator": "policy",
"assurance": "deterministic",
"message": "Deadline missed by 7 day(s): Mastercard allows 45 calendar days to respond. Received 2026-07-28, due 2026-09-11, issued 2026-09-18.",
"quote": "",
"expected": "on or before 2026-09-11",
"observed": "2026-09-18",
"rule_id": "NET-REP-WINDOW",
"citation": {
"pack": "disputes-us@0.2.0",
"rule_id": "NET-REP-WINDOW",
"cite": "Visa Dispute Management Guidelines (dispute response); Mastercard Chargeback Guide (second presentment)",
"source": "Visa Core Rules and Visa Product and Service Rules; Visa Dispute Management Guidelines",
"url": "https://usa.visa.com/support/small-business/dispute-resolution.html",
"review_status": "unreviewed"
},
"record_ref": "chargeback:CB-77120#chargeback_date",
"confidence": 1.0,
"related": [],
"evidence_ref": "EV-2026-1M2VZHKBYRGPYK",
"label": null,
"label_note": null,
"labeled_by": null
}
]
}Step 6
Handle hold, fail, and errors
Hold. Park the case. A reviewer opens the validation in the console at the exact sentence and releases, blocks, or returns it with a written rationale. Your system learns the decision from the validation.reviewed webhook, or by reading review_status.
Fail. Do not send the draft. Give the findings back to your agent, redraft, and validate the new draft as a new request with a new idempotency key. A failed validation can be released by a reviewer only after every critical finding has been labeled.
Status failed. If the validator itself errors, the validation has status: "failed" and an error string, and a validation.failed webhook is sent. Treat it as unvalidated.
No response. If the API is unreachable or returns 5xx, your workflow decides: wait and retry with the same idempotency key, or send the case to a person. Log which one you did.
Your service key cannot review or release. Those are actions for people, with their own keys or the console.
v = atq.validations.create(...)
if v["status"] == "failed":
# The validator itself errored (v["error"]). Apply your own fallback:
# most teams route the draft to a person rather than send it unchecked.
route_to_reviewer(case_id, reason="validation_failed")
elif v["verdict"] in ("pass", "pass_with_warnings"):
send_to_network(draft_text) # warnings are releasable; log them
elif v["verdict"] == "hold":
# review_status is "pending": a reviewer releases, blocks, or returns it.
park_until_reviewed(case_id, v["id"]) # wait for the validation.reviewed webhook
else: # "fail"
# A critical finding. Give the findings back to the agent and validate the new draft.
redraft(case_id, must_fix=[(f["code"], f["message"], f["expected"]) for f in v["findings"]])Step 7
Async mode and webhooks
Send mode: "async" to get a 202 immediately with status: "queued". Before the job is queued, the draft and records are redacted and card numbers masked; the job payload is purged when the job finishes. The verdict arrives by webhook.
Register an endpoint
POST /v1/webhooks with an HTTPS URL and the events you want (an empty list means all). The secret is returned once. URLs that resolve to private or internal addresses are refused, at registration and again at each delivery. This needs webhooks:manage, which the service key does not have.
| Event | Sent when |
|---|---|
| validation.completed | A validation finished and needs no review (pass, pass_with_warnings). |
| validation.held | A validation finished and waits for review (hold, fail). Sent instead of validation.completed. |
| validation.reviewed | A reviewer released, blocked, or returned a validation. |
| validation.failed | The validator itself errored (status failed). Treat the draft as unvalidated. |
| authorize.recommendation.created | Authorize (preview) created a recommendation. |
| report.created | An attestation report was generated. |
Verify every delivery
Each POST carries Attestiq-Event, Attestiq-Delivery (the delivery id), and Attestiq-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 with your secret over "<t>." + raw body. Verify against the raw bytes, and reject timestamps more than five minutes old.
Return any 2xx to acknowledge. Anything else, or a timeout after 5 seconds, is retried with exponential backoff, up to 8 attempts. Because of retries, deduplicate on Attestiq-Delivery. Webhook payloads carry finding codes and messages, not the draft or the records.
curl https://api.attestiq.ai/v1/validate/validations \
-H "X-API-Key: $ATTESTIQ_API_KEY" -H "Content-Type: application/json" \
-d '{"workload": "disputes.representment", "mode": "async", ... }'
HTTP/1.1 202 Accepted
{"id": "val_01a0b7f8cd8f3a36b3c8b686", "object": "validation",
"status": "queued", "verdict": null, "score": null, "findings": [], ...}# Needs a key with webhooks:manage (engineer or admin), not the service key.
curl https://api.attestiq.ai/v1/webhooks \
-H "X-API-Key: $ATTESTIQ_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/attestiq",
"events": ["validation.completed", "validation.held", "validation.reviewed", "validation.failed"]}'
HTTP/1.1 201 Created
{"id": "whe_01a0b7f8cd9c49bc2093b2cb",
"url": "https://hooks.example.com/attestiq",
"events": ["validation.completed", "validation.held", "validation.reviewed", "validation.failed"],
"secret": "whsec_…"} # shown once — store it in your secret managerPOST /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"}import json, os
from attestiq_sdk import verify_webhook
from fastapi import FastAPI, HTTPException, Request # any framework works; use the raw body
app = FastAPI()
SECRET = os.environ["ATTESTIQ_WEBHOOK_SECRET"] # the whsec_… value
@app.post("/attestiq")
async def attestiq_webhook(request: Request):
body = await request.body() # raw bytes, before any JSON parsing
if not verify_webhook(SECRET, body, request.headers.get("Attestiq-Signature", "")):
raise HTTPException(status_code=400) # bad signature or older than 5 minutes
delivery_id = request.headers["Attestiq-Delivery"]
if already_processed(delivery_id): # retries can deliver the same event again
return {"ok": True}
event = json.loads(body)
handle(event["type"], event["data"]) # e.g. release or hold the case in your system
return {"ok": True} # any 2xx stops retriesStep 8
Idempotency keys
Set idempotency_key in the body (not a header) to a value unique per draft, such as your case id plus a draft number. Keys are scoped to your tenant.
- Same key, same body:
200with the original validation and the headerIdempotent-Replayed: true. Nothing is re-run or re-billed. - Same key, different body:
409 conflict. The comparison ignoresmode.
This makes retries after a timeout safe. A redrafted document is a new draft: give it a new key.
# First call: 201 Created
# Same key, same body: 200 OK with the original validation
HTTP/1.1 200 OK
Idempotent-Replayed: true
# Same key, different body:
HTTP/1.1 409 Conflict
{"code": "conflict", "message": "idempotency key reused with a different request body"}Step 9
Going live checklist
- The service key is in a secret manager, not in code, prompts, or tool arguments.
- Your record mapping sends only the minimum fields. A test with a full card number gets
422 pci_data_rejected. - Every request sets
external_idand a per-draftidempotency_key. - Your workflow handles all four verdicts,
status: "failed", 422, 429, and timeouts. - If you use async mode: the webhook endpoint is public HTTPS, verifies signatures, deduplicates by delivery id, and returns 2xx quickly.
- Your admin has chosen a storage mode and a retention period.
- You have run your agent's historical drafts through
POST /v1/validate/evalsand reviewed precision and recall per error code with your analysts. - You store the validation
idandverdict_refon the case, so an examiner can trace any document to its evidence. - Reviewers have user keys (or console access) with the right roles.