InvoanceInvoance
Log inStart free
Developers
Search docs…⌘K
Getting started
OverviewConceptsAuthenticationCreate an API key
API reference
EndpointsErrors
Audit Logs
Quick startEvent schemaExporting eventsSDK reference
AI Attestations
Quick startAttestation schemaVerification & proofSDK reference
Resources
EventsDocumentsTraces
SDKs
PythonNode.jscURL
Verification
How it works
Support
API FAQ

Attestation schema

Every attestation you send to POST /v1/ai/attestationsuses one strict envelope: what went into the model, what came out, and which model produced it. From that request the server derives three SHA-256 hashes, stores a byte-identical canonical copy, and signs a compact statement over the hashes with your tenant's Ed25519 key. This page walks through each layer: the request you send, the three hashes, the signed statement, the stored record, and what happens when you send the same thing twice.

Base URL https://api.invoance.com/v1·Auth Authorization: Bearer invoance_live_... or X-API-Key (write scope to ingest)

The request envelope

Exactly what you send, with every field populated: the payload pair, the model identity, an optional subject (who or what the output was about), and an optional trace_id to file the attestation into an open trace. The Idempotency-Key header is optional but recommended for safe retries.

POST /v1/ai/attestations
curl -X POST https://api.invoance.com/v1/ai/attestations \
  -H "Authorization: Bearer invoance_live_xxx" \
  -H "Idempotency-Key: 9c2f6a1e-8f4b-4c3a-b1d2-7e5a9c0d4f6b" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "output",
    "payload": {
      "input": "Summarize the attached contract for a non-lawyer.",
      "output": "This agreement covers a 12-month SaaS subscription..."
    },
    "context": {
      "model_provider": "openai",
      "model_name": "gpt-4o",
      "model_version": "2024-11-20"
    },
    "subject": {
      "user_id": "user_42",
      "session_id": "sess_9d1c",
      "ticket": "SUP-1042"
    },
    "trace_id": "018f6b2a-7c4d-7e9a-b3f1-2a5c8d9e0f11"
  }'

The 201 response returns the id and all three hashes so you can store them alongside your own records:

{
  "attestation_id": "018f6b2a-9a3e-7c21-8d4f-5b6a7c8d9e0f",
  "created_at": "2026-07-04T12:00:00Z",
  "input_hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
  "output_hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
  "payload_hash": "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
  "status": "accepted"
}
FieldTypeRequiredNotes
typestring enumYesOne of output | decision | approval. Anything else is rejected 400 invalid_attestation_type.
payloadobjectYesThe exact input/output pair being attested.
payload.inputstringYesWhat went into the model, verbatim. Non-empty (400 empty_payload). Hashed as input_hash.
payload.outputstringYesWhat the model produced, verbatim. Non-empty (400 empty_payload). Hashed as output_hash.
contextobjectYesModel identity at the moment of the call. It becomes part of the signed statement.
context.model_providerstringYese.g. openai, anthropic, self-hosted. Non-empty (400 invalid_context).
context.model_namestringYese.g. gpt-4o. Non-empty (400 invalid_context).
context.model_versionstringYese.g. 2024-11-20. Non-empty (400 invalid_context).
subjectobjectNoWho or what the output was about. Whole block ≤ 8 KB serialized (400 subject_too_large).
subject.user_idstringNoYour end-user id. Surfaced on the public proof page.
subject.session_idstringNoYour session/conversation id. Surfaced on the public proof page.
subject.* (extras)JSON valuesNoUp to 20 extra keys beyond user_id/session_id (400 subject_too_many_keys). Stored and hashed in alphabetical key order.
trace_idstring (UUID)NoFiles the attestation into one of your traces. The trace must exist (404 trace_not_found) and be open (409 trace_not_open once sealing has started).
  • The whole request body must serialize to ≤ 1 MB (413 payload_too_large). Attest large payloads by sending the exact text you want provable, not attachments.
  • The Idempotency-Key header is optional; when present, retries with the same key and body replay the original response (see the last section).
  • Ingestion is asynchronous: a 201 means the attestation is durably queued and its hashes are final, but a GET immediately afterwards can 404 for a moment until the writer persists the row.

The three hashes

Every attestation carries three SHA-256 digests, computed synchronously at ingest and returned in the 201 response. Anyone holding the original text can recompute them and compare, no Invoance tooling required.

HashSHA-256 overNotes
input_hashThe UTF-8 bytes of payload.inputRecompute locally: sha256 of the exact input string must match.
output_hashThe UTF-8 bytes of payload.outputSame recipe: sha256 of the exact output string.
payload_hashThe canonical request bytes (below)The root hash. Also stored as attestation_hash, unique per tenant, and used as the deduplication key.

Canonical bytes are not your raw wire bytes. The server parses your request, then re-serializes it in a fixed struct field order: type, payload (input, output), context (model_provider, model_name, model_version), subject (user_id and session_id when present, then extra keys alphabetically), trace_id. Absent optional fields are omitted entirely, whitespace is compacted, and key order is normalized, so the same content always produces the same bytes regardless of how you formatted the request:

{"type":"output","payload":{"input":"Summarize the attached contract for a non-lawyer.","output":"This agreement covers a 12-month SaaS subscription..."},"context":{"model_provider":"openai","model_name":"gpt-4o","model_version":"2024-11-20"},"subject":{"user_id":"user_42","session_id":"sess_9d1c","ticket":"SUP-1042"},"trace_id":"018f6b2a-7c4d-7e9a-b3f1-2a5c8d9e0f11"}

A byte-identical copy of these canonical bytes is stored in object storage and served back verbatim, so hashing exactly what the raw endpoint returns reproduces payload_hash:

GET/v1/ai/attestations/{attestation_id}/rawReturns the exact canonical JSON bytes that were hashed at ingest.

The signed statement

What gets signed is not your payload itself but a compact, server-built canonical statement that binds the three hashes to your tenant, the model identity, and the accept time. It is serialized in exactly this field order:

{
  "v": 1,
  "attestation_id": "018f6b2a-9a3e-7c21-8d4f-5b6a7c8d9e0f",
  "tenant_id": "0189a7c2-4f6e-7b3d-9a1c-8e2f5d6a7b4c",
  "attestation_type": "output",
  "input_hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
  "output_hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
  "payload_hash": "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
  "model_provider": "openai",
  "model_name": "gpt-4o",
  "model_version": "2024-11-20",
  "created_at": "2026-07-04T12:00:00Z"
}

These exact bytes are stored as signed_payloadand signed directly with your tenant's per-tenant Ed25519 key (plain Ed25519, no pre-hash). The 64-byte signature and the verifying public_key are returned on GET. Because the statement contains payload_hash, the signature transitively covers every byte of your original request.

One honesty note: the SDKs' verify_signature() / verifySignature() fetch the record from the API and check the signature against the public_keyembedded in that same response. That proves the record is internally consistent, but it trusts the server it fetched from — it is not an offline check. For a check that does not take the record's word for the key, compare against the key Invoance publishes for your verified domain:

GET/keys/{domain}Public, unauthenticated tenant signing key (base64url Ed25519), served from the write-once key registry. Note: root level, no /v1 prefix.

The stored record

The persisted attestation, as returned by GET /v1/ai/attestations/{attestation_id}. Rows live in an append-only ledger: database triggers reject DELETE, and retention is handled by sealing, never by removal.

FieldTypeNotes
attestation_idUUIDMinted at ingest; the id used in every endpoint path and public proof URL.
tenant_idUUIDYour tenant. Also inside the signed statement.
attestation_typestringoutput | decision | approval.
attestation_hashhex SHA-256The root hash. Equal to payload_hash; unique per tenant and used as the dedup key.
input_hashhex SHA-256Hash of payload.input.
output_hashhex SHA-256Hash of payload.output.
signed_payloadhex bytesThe exact canonical statement bytes that were signed (previous section).
signaturehex (64 bytes)Ed25519 signature over signed_payload.
signature_algstringAlways ed25519.
public_keyhex (32 bytes)Verifying key snapshot stored on the row. The public proof and verify endpoints deliberately serve the write-once registered tenant key instead; cross-check against GET /keys/{domain}.
model_provider / model_name / model_versionstring | nullCopied from context at ingest.
subject_user_id / subject_session_idstring | nullFrom subject; shown on the public proof page.
trace_idUUID | nullSet when the attestation was ingested into an open trace; its hash is included in that trace's composite seal hash.
retention_policystringshort | standard | extended | regulatory | indefinite, derived from your plan's retention window at write time.
expires_atRFC3339 | nullcreated_at + retention window; null when retention is indefinite.
access_tierstringactive | sealed. See below.
created_atRFC3339Server accept time; this is the timestamp inside the signed statement.
organizationobjectIssuer identity attached on GET: name, issuer_name, primary_domain, domain_verified (+ verified date and logo when set).

Sealed rows. When the retention window ends, the retention worker flips access_tier to sealed instead of deleting. Sealed attestations disappear from the list endpoint and return 410 retention_expired on GET and on /raw, but they remain verifiable: the verify endpoints and the public proof page keep working, and a plan upgrade unseals them.

Deduplication and idempotency

Two independent layers keep retries safe. Content-level dedup keys on payload_hash per tenant and needs no header; the Idempotency-Key header adds exact response replay on top.

You sendResponseWhat it means
The same body again (any or no Idempotency-Key)200 status: "duplicate"Content dedup: identical canonical bytes → identical payload_hash → the original attestation_id is returned. One record, ever.
The same Idempotency-Key + the same bodyoriginal response, replayedThe completed response is cached and replayed verbatim for 24 hours.
The same Idempotency-Key + a different body409 idempotency_key_reuse_mismatchKey reuse with changed content is rejected; nothing new is stored.
A retry while the first request is still in flight202 status: "processing"The original holds a reservation (self-clears within 120 s). Back off and retry; you will then hit the replay or dedup path.

Because dedup keys on content alone, two genuinely identical attestations collapse into one record. If you need distinct records for identical input/output pairs (say, the same answer served to two requests), add a distinguishing subject key such as a request id — it changes the canonical bytes and therefore the payload_hash.

Next steps

AI Attestations quick startVerifying attestationsSDK reference
Invoance

Neutral digital proof infrastructure for business. Tamper-evident, independently verifiable records.

Subscribe to our newsletter

Products
Platform
How It Works
Developers
Verify
Resources
Help & Legal
Products
  • Event Ledger
  • Document Anchoring
  • AI Attestation
  • Audit Logs
Platform
  • Why Invoance
  • For Compliance Teams
  • For Finance Teams
  • Pricing
How It Works
  • Overview
  • Event Ledger
  • Document Anchoring
  • AI Attestation
Developers
  • Overview
  • Endpoints
  • Authentication
  • Concepts
Verify
  • Verify Document
  • Verify AI Attestation
  • Verify Event
  • Verify Trace
Resources
  • All Resources
  • SOC 2 Guide
  • HIPAA Guide
  • ISO 27001 Guide
Help & Legal
  • Support
  • Status
  • Verification Help
  • FAQ

Invoance provides technical verification and proof infrastructure for digital records. Invoance does not issue legal, financial, or regulatory advice.

Records anchored through Invoance are cryptographically signed and tamper-evident by design. Invoance does not verify the accuracy, legality, or authenticity of document contents, only that a record existed in a specific form at a specific time. Verification links are publicly resolvable and do not require authentication. Invoance does not act as a custodian of funds, a legal authority, or a regulated financial entity. Use of Invoance does not constitute legal compliance. Consult qualified counsel for your specific obligations.

© 2025 – 2026 Invoance, Inc. All rights reserved.••