Attestation schema
What you send, the bytes the server hashes, and the bytes the tenant key signs.
An attestation records one model call: the input text, the output text, the model provider, name and version, and an optional subject.
The server hashes the input, the output and the whole request. A worker then signs a short payload built from those hashes with the tenant's Ed25519 key.
The get endpoint returns the hashes, the signed bytes, the signature and the public key. The raw endpoint returns the request bytes. Both are stored as written, never rebuilt.
- input
- What is the company refund policy?
- output
- Our refund policy allows returns within 30 days of delivery…
- model
- openai · gpt-4.1 · 2026-01-01
- input_hash
- 2c8f05d1…a7e4409b
- output_hash
- f06b3a9e…11d27c58
- subject
- user_7b1c · sess_4f9a
Five top level fields. type, payload and context are required; subject and trace_id are optional.
Every field is part of the hashed bytes, including a null subject or trace_id.
| output | The record attests a model output. |
|---|---|
| decision | The record attests a decision taken on a model output. |
| approval | The record attests an approval of a model output. |
typeWhat the record attests: a model output, a decision taken on it, or an approval of it. One of output, decision, approval.
trace_idAn open trace owned by the tenant; the attestation is attached to it and included when the trace is sealed.
payloadThe text that is hashed.
payload.inputThe prompt or input text; its SHA-256 becomes input_hash.
payload.outputThe model's output text; its SHA-256 becomes output_hash.
contextWhich model produced the output.
context.model_providerProvider name, for example openai or anthropic; also a list filter.
context.model_nameModel name as the provider names it.
context.model_versionModel version or snapshot date.
subjectWho or what triggered the call; every key becomes part of the hashed bytes.
subject.user_idYour identifier for the end user.
subject.session_idYour identifier for the session or conversation.
subject.*Any other keys with JSON values, kept as custom context and written in sorted key order.
The server does not hash the bytes you sent. It parses the request and writes it again as compact JSON. Those bytes are hashed, stored, and returned by the raw endpoint.
- Fields in this order: type, payload, context, subject, trace_id.
- No whitespace between tokens.
- A missing subject or trace_id is written as null.
- Inside subject: user_id, then session_id, each only when sent, then custom keys sorted by key.
- Only quotes, backslashes and control characters are escaped. Other characters are written as UTF-8.
- More than 1 MB of canonical bytes returns 413 payload_too_large.
To reproduce payload_hash, hash the raw endpoint's body as returned. Rebuilding the bytes yourself works only if every rule above holds.
{
"type": "output",
"payload": {
"input": "Summarize the termination clause in contract CT-8472.",
"output": "Either party may terminate with 30 days written notice. Early termination fees do not apply after month 12."
},
"context": {
"model_provider": "openai",
"model_name": "gpt-4.1",
"model_version": "2026-04-14"
},
"subject": {
"user_id": "u_4821",
"session_id": "sess_9f3a",
"department": "legal"
}
}
Three SHA-256 values are computed at ingest and returned in the 201 response. Each one covers a single byte string; nothing is concatenated.
Anyone holding the text or the raw bytes can recompute them with a standard SHA-256 and no Invoance tooling. The example values on this page check out against the get endpoint's record.
input_hash | SHA-256 of the UTF-8 bytes of payload.input, hex encoded. |
|---|---|
output_hash | SHA-256 of the UTF-8 bytes of payload.output, hex encoded. |
payload_hash | SHA-256 of the canonical bytes, hex encoded. Returned by ingest. |
attestation_hash | The same value as payload_hash, stored as the row's key. It is unique per tenant, so the same bytes twice return the existing record. |
After the write queue accepts the record, a worker builds a second compact JSON object and signs it. That object is signed_payload.
- The signature is plain Ed25519 over these bytes. No prehash, no domain tag.
- The key is the tenant's signing key. public_key is its raw 32-byte public key, hex encoded.
- The input, output and subject are not in the signed bytes. payload_hash binds them.
- Verify the hex decoded bytes exactly as the get endpoint returns them.
- created_at in the signed bytes is the string the worker signed. A payload rebuilt from the other fields can differ in it.
| v | The payload version. Always 1. |
|---|---|
attestation_id | The id returned by ingest. |
tenant_id | The tenant that owns the record. |
attestation_type | The type sent at ingest. |
input_hash | As above. |
output_hash | As above. |
payload_hash | As above. This field binds the input, output and subject to the signature. |
model_provider | context.model_provider as sent. |
model_name | context.model_name as sent. |
model_version | context.model_version as sent. |
created_at | When the server accepted the request. RFC 3339 in UTC with a Z suffix; real values carry fractional seconds. |
{
"v": 1,
"attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"tenant_id": "3b9d6f10-52c4-4a7e-9e1b-8d0c2f4a6e71",
"attestation_type": "output",
"input_hash": "a46f0ad5dfb45fcbe2c303dcb34292af2f63fd79c6ff45b52dc40703a1cc5a70",
"output_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
"payload_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
"model_provider": "openai",
"model_name": "gpt-4.1",
"model_version": "2026-04-14",
"created_at": "2026-09-22T08:14:07Z"
}
Identical canonical bytes give the same payload_hash, and one tenant holds one record per hash. An Idempotency-Key adds exact response replay on top.
To keep two identical calls apart, add a subject key such as a request id. It changes the canonical bytes and the hash with them.
Full replay rules are on the ingest endpoint.
| Same body, with or without a key | 200 with status duplicate and the existing attestation_id. Nothing new is written. |
|---|---|
| Same Idempotency-Key, same body | The first response, replayed for 24 hours. |
| Same Idempotency-Key, different body | 409 idempotency_key_reuse_mismatch. |
| Retry while the first request is running | 202 with a body of {"status":"processing"}. Retry after a moment. |
Are the input and output stored?
Yes. The canonical bytes are stored in object storage and returned by the raw endpoint. The get and list endpoints return only the hashes.
Why does my recomputed payload_hash differ?
You hashed different bytes. Pretty printing, another key order or an unsorted subject changes the hash. Hash the raw endpoint's body as returned.
What happens when retention ends?
The row is sealed, not deleted. It leaves the list, get and raw return 410 retention_expired, verify still answers, and a plan upgrade can unseal it.