The Receipt
Every routed call produces one signed record. This page documents that record in full: its fields, how the signed bytes are constructed, and how to check a signature without calling Aqta.
The format is ATTESTATION-v1. It is published as an open specification and described in an IETF Internet-Draft, draft-chueayen-attestation-receipts. An Internet-Draft is a submission, not a standard. The point of publishing it is that you can implement a verifier yourself, and that nothing here depends on our goodwill.
Where the receipt appears
On an OpenAI-compatible call, the receipt is returned on the response body under aqta.attestation. Most SDKs discard unrecognised fields, so read the raw response.
pythonraw = client.chat.completions.with_raw_response.create( model="gpt-4o", messages=[{"role": "user", "content": "..."}], ) receipt = raw.http_response.json()["aqta"]["attestation"] print(receipt["outcome"]) # ALLOWED or BLOCKED
A blocked call still returns a receipt. That is deliberate: the record of a decision not to proceed is usually the one an auditor asks for.
Fields
Twelve fields, all required. A receipt missing any of them is not a valid ATTESTATION-v1 receipt and a conforming verifier will reject it.
| Field | Type | Meaning |
|---|---|---|
v | integer | Format version. Always 1 for this specification. |
attestation_id | string | UUID v4, unique per receipt. |
trace_id | string | Issuer-assigned identifier for the underlying model call. |
org_id | string | Identifier of the subject organisation. |
request_hash | string | SHA-256 of the canonicalised request body, as 64 lowercase hex characters. |
model | string | Provider-qualified model identifier, for example gpt-4o. |
outcome | string | One of the values below. |
policy_applied | array | Policy identifiers that ran, sorted lexicographically. |
cost_prevented_eur | number | Issuer estimate of spend avoided by a blocked or suppressed call, in EUR. Non-negative, six decimal places. |
timestamp | string | ISO 8601 with an explicit timezone offset. |
public_key | string | Base64url raw 32-byte Ed25519 public key of the issuer, unpadded. |
signature | string | Base64url raw 64-byte Ed25519 signature, unpadded. Excluded from the signed bytes. |
The receipt records the decision, not the conversation. Prompt and completion text are never included. request_hash lets you prove a specific request produced this receipt if you still hold the request; it does not let anyone reconstruct the request from the receipt.
Outcome values
| Value | Meaning |
|---|---|
ALLOWED | Policy passed. The request proceeded to the provider. |
BLOCKED | Policy blocked the request before the provider was called. |
SUPPRESSED | A runaway condition, such as a loop, was detected and the call was suppressed. |
PASSED | Synonym of ALLOWED, retained for older receipts. New issuers should emit ALLOWED. |
Since 11 August 2026 the gateway emits ALLOWED on the proxy path; receipts issued before that date carry PASSED. Treat the two as the same outcome when you write a check, and never match on one alone.
Canonical bytes
The signature covers a canonical serialisation, not whatever JSON happened to arrive over the wire. To reconstruct the signed bytes:
- Remove the
signaturefield. - Serialise the remaining fields to JSON with keys sorted lexicographically and no whitespace between tokens.
- Emit non-ASCII characters literally as UTF-8, never as
\uXXXXescapes. - Serialise integer-valued numbers without a decimal point or trailing zeros.
- Encode the result as UTF-8 bytes.
Steps 3 and 4 exist because the two mainstream JSON serialisers disagree by default, and the disagreement is silent. Python's json.dumps escapes non-ASCII unless you pass ensure_ascii=False, while JavaScript's JSON.stringify never escapes it. Python renders 0.0 where JavaScript renders 0. An issuer that takes either default produces receipts that verify in its own language and fail everywhere else, which is the worst possible failure mode for evidence. Both rules are mandatory.
pythonimport json payload = {k: v for k, v in receipt.items() if k != "signature"} cost = round(payload["cost_prevented_eur"], 6) payload["cost_prevented_eur"] = int(cost) if cost == int(cost) else cost canonical = json.dumps( payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False ).encode("utf-8")
Verifying
A receipt carries its own public_key. Checking a signature against that embedded key proves only that whoever made the receipt held some key, and anyone can self-sign a forgery. Real verification pins the issuer key out of band and compares.
- Obtain the issuer public key out of band and pin it in config, a secret store, or a key list you control.
- Compare the receipt's
public_keyto the pinned key byte for byte. Reject on mismatch. - Decode
signaturefrom base64url to 64 bytes. - Rebuild the canonical payload bytes.
- Verify the Ed25519 signature over those bytes with the pinned key, using a constant-time routine.
Our published key is available at https://api.aqta.ai/v1/attestation/public-key, with the raw key mirrored at https://app.aqta.ai/security/pubkey.txt. Fetch it once at setup or on a documented rotation. A verification loop that re-fetches the key on every check has quietly reintroduced a dependency on us, which defeats the purpose.
Verifier packages
Two reference implementations are published, one for each ecosystem. Both implement this specification and cross-check each other's output in CI.
bashnpx aqta-verify-receipt receipt.json --key <pinned-key>
bashpip install aqta-verify-receipt aqta-verify-receipt receipt.json --key <pinned-key>
Both exit 0 on a valid receipt and non-zero on failure, so they drop into a pipeline without parsing output. You do not need either one: the five steps above are enough to write your own in any language with an Ed25519 library, and an auditor who prefers their own tooling should.
Example
json{ "v": 1, "attestation_id": "a3f2b1c4-9d87-4e6f-b012-34567890abcd", "trace_id": "trace-2026-04-23-abc123", "org_id": "org-acme-bank", "request_hash": "8f3a7e2b9c4d5f6a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a", "model": "gpt-4o", "outcome": "ALLOWED", "policy_applied": ["budget_guard", "loop_guard"], "cost_prevented_eur": 0, "timestamp": "2026-04-23T10:15:30.123456+00:00", "public_key": "gUoUhIvptKAoLTnry3VrDtOQEWggGQveLrHFVrfNqmE", "signature": "..." }
Sample records to verify against are published with the specification: every published conformance vector covering both record formats, 25 valid and 25 that must fail. You can check them offline with either package above.
What a receipt is not
A receipt is evidence that a specific decision was made under specific rules at a specific time, signed by a key the model provider does not hold. It is not a certification, an assurance opinion, or a substitute for your own control programme. It is the artefact those things get built on.