Attestiq
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 hold and fail, 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).

Try it without a key
# 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.

GET /v1/me
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"
}
KeyRoleCanCannot
Service key (your agent, the MCP server)serviceSubmit validations; read validations, evidence, policy packsReview, 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, settingsAnything outside the role

Step 3

Environments

There is one API. Your key decides which tenant you reach.

EnvironmentWhat it isData
Public sandbox/v1/demo/*, no key. Runs the validator in memory on built-in samples.Synthetic only; nothing stored
Sandbox tenantA tenant marked synthetic: tenant.synthetic is true in GET /v1/me. Pilots start here.Synthetic or de-identified historical drafts
Production tenantYour 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.

POST /v1/validate/validations
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
FieldRequiredWhat to send
workloadyesSelects the checks and the policy pack: disputes.representment, disputes.cardholder_letter, authorize.retry_policy, authorize.recommendation. GET /v1/validate/taxonomy lists them.
agentyes{id, version?, model?}. Used for per-agent reporting.
outputyesThe draft as text, or a JSON object for structured workloads. Up to 60,000 characters by default.
recordsper workloadUp to 50 {type, id, data} records. A missing required type returns 422 naming it.
external_idnoYour case or document id. Echoed in webhooks.
idempotency_keyrecommendedOne per draft. See step 8.
modenosync (default) or async.
policy_packnoPin a pack version, e.g. disputes-us@0.2.0. Default: the latest for the workload.
as_ofnoYYYY-MM-DD evaluation date when the draft carries none.

Step 5

Read the verdict

verdictMeaningreview_status
passNo finding at medium severity or above.not_required
pass_with_warningsMedium findings only. Releasable; review the notes.not_required
holdAt least one high finding. A reviewer must decide.pending
failAt 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 / observed and record_ref: the value in your record and where it is (chargeback:CB-77120#chargeback_date).
  • rule_id and citation: the rule and its source. citation.review_status says whether an expert has reviewed that rule.
  • assurance: how it was established — deterministic, model_extracted, model_judged, or advisory (informational, never affects the verdict).
  • evidence_ref: the signed evidence record. Store id and verdict_ref with your case.
201 Created
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.

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

EventSent when
validation.completedA validation finished and needs no review (pass, pass_with_warnings).
validation.heldA validation finished and waits for review (hold, fail). Sent instead of validation.completed.
validation.reviewedA reviewer released, blocked, or returned a validation.
validation.failedThe validator itself errored (status failed). Treat the draft as unvalidated.
authorize.recommendation.createdAuthorize (preview) created a recommendation.
report.createdAn 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.

Async request
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": [], ...}
Register an endpoint
# 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 manager
What you receive
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"}
Verify the signature
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 retries

Step 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: 200 with the original validation and the header Idempotent-Replayed: true. Nothing is re-run or re-billed.
  • Same key, different body: 409 conflict. The comparison ignores mode.

This makes retries after a timeout safe. A redrafted document is a new draft: give it a new key.

Replays
# 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

  1. The service key is in a secret manager, not in code, prompts, or tool arguments.
  2. Your record mapping sends only the minimum fields. A test with a full card number gets 422 pci_data_rejected.
  3. Every request sets external_id and a per-draft idempotency_key.
  4. Your workflow handles all four verdicts, status: "failed", 422, 429, and timeouts.
  5. If you use async mode: the webhook endpoint is public HTTPS, verifies signatures, deduplicates by delivery id, and returns 2xx quickly.
  6. Your admin has chosen a storage mode and a retention period.
  7. You have run your agent's historical drafts through POST /v1/validate/evals and reviewed precision and recall per error code with your analysts.
  8. You store the validation id and verdict_ref on the case, so an examiner can trace any document to its evidence.
  9. Reviewers have user keys (or console access) with the right roles.