Home
Home/Developers/AI Attestations
Ed25519 signaturesSHA-256 hashes8 SDKs
Status
Sign inStart free
Home
Start here
Overview
Authentication
Errors
FAQ
API
Events
Canonical JSON and hashes
Documents
Anchoring a file
AI attestations
Attestation schema
Verifying attestations
Traces
Sealing a trace
Audit logs
Organizations
Streams
Portal
Public proof
Event schema
Exporting events
Integrations
Clerk
Auth0
Embeddable viewer
All endpoints
Reference
SDKs
Node.js
Python
Go
Java
Ruby
Rust
.NET
PHP
REST
Verification
API · AI Attestations

AI Attestations

Signed records of what a model was asked, what it answered, and which model answered.

sha-256 · 3a352297…2592
sha-256 · eff042e3…d767

An attestation is a record of one model call: the input text, the output text, the model provider, name and version, and an optional subject describing who or what triggered it. You post the record and the backend hashes the input, the output and the whole request, stores the request bytes, and returns the hashes with an attestation id.

A worker then signs a compact payload made of the record's id, tenant, type, the three hashes, the model context and the creation time with the tenant's Ed25519 key, and writes the row to an append-only table. The signed payload, signature and public key are returned by the get endpoint and can be checked offline; every SDK has a signature verification helper for it.

To check that a piece of text is the one that was recorded, hash it and post the hash to the verify endpoint, or fetch the raw payload and compare its SHA-256 with attestation_hash. Records past their plan's retention window are sealed: they leave the list and return 410 from get and raw, while verify still answers.

Endpoints
POST/v1/ai/attestationsIngest an attestationGET/v1/ai/attestationsList attestationsGET/v1/ai/attestations/{attestation_id}Get an attestationGET/v1/ai/attestations/{attestation_id}/rawGet the raw payloadPOST/v1/ai/attestations/{attestation_id}/verifyVerify a hash
Limits
Request body as canonical JSON1 MB (1,048,576 bytes); larger returns 413 payload_too_large
subject block8 KB serialized
subject custom keys20, not counting user_id and session_id
typeoutput, decision or approval
payload.input, payload.output and the three context fieldsmust not be empty or whitespace only
List page size1 to 500, default 50
List cache30 seconds per tenant and query
Raw payload cache24 hours per tenant and attestation
Verify hash cache60 seconds per attestation
content_hashexactly 64 hex characters
Idempotency-Key replay window24 hours after the first response; 120 seconds while the first request is in flight
Monthly ingest quotathe plan's ai_attestations_per_month plus applied top-ups, counted per billing period
Monthly verify quotathe plan's api_verifications_per_month plus applied top-ups, counted per billing period
Rate limitthe plan's rate_limit_per_sec per tenant (default 10) and 60 times that per minute; exceeding either returns 429 rate_limited with Retry-After
Endpoints
POST/v1/ai/attestations
API keywriteIdempotency-Key

Ingest an attestation

Hashes a model input and output with its model context, queues the record for signing and returns the attestation id and hashes.

Headers
Content-Typestring · required

Must be application/json; any other value is rejected by the framework with 415 before the handler runs.

Idempotency-Keystring

Replays the first response for 24 hours when the same key is sent again with the same body; the value is trimmed and an empty value is ignored.

Request body
typestring · required

What the record attests: a model output, a decision taken on it, or an approval of it. One of output, decision, approval.

payloadobject · required

The text that is hashed.

payload.inputstring · required · must not be empty or whitespace only

The prompt or input text; its SHA-256 becomes input_hash.

payload.outputstring · required · must not be empty or whitespace only

The model's output text; its SHA-256 becomes output_hash.

contextobject · required

Which model produced the output.

context.model_providerstring · required · must not be empty or whitespace only

Provider name, for example openai or anthropic; also a list filter.

context.model_namestring · required · must not be empty or whitespace only

Model name as the provider names it.

context.model_versionstring · required · must not be empty or whitespace only

Model version or snapshot date.

subjectobject · up to 8 KB serialized, at most 20 keys besides user_id and session_id

Who or what triggered the call; every key becomes part of the hashed bytes.

subject.user_idstring

Your identifier for the end user.

subject.session_idstring

Your identifier for the session or conversation.

subject.*object · at most 20 keys

Any other keys with JSON values, kept as custom context and written in sorted key order.

trace_iduuid

An open trace owned by the tenant; the attestation is attached to it and included when the trace is sealed.

  • The canonical bytes that are hashed are the request re-serialized by the server: compact JSON with the fields in the order type, payload, context, subject, trace_id; a missing subject or trace_id is written as null, user_id and session_id are omitted when absent, and custom subject keys are written in sorted order.
  • The response is sent once the record is on the write queue; a worker signs it and writes the row, so GET can return 404 for a short time after the 201.
  • The signature is not in this response; read it with GET /v1/ai/attestations/{attestation_id} once the worker has written the row.
  • When the tenant already has a record with the same payload_hash, the call returns 200 with status duplicate and the existing attestation_id and created_at, and nothing new is written.
  • With an Idempotency-Key, the first response is replayed for 24 hours; a retry while the first request is still in flight gets 202 with a body of {"status":"processing"}, and the same key with a different body gets 409.
  • Idempotency keys are scoped to the tenant, the API key and this path in Redis and Postgres; the in-process cache keys on the tenant, the API key and the value only, so a key reused on another endpoint with a different body can get 409 from the same server process while that cache entry lives.
  • A 200 duplicate response is not stored under the Idempotency-Key, so a retry with the same key inside 120 seconds gets 202 processing until the reservation expires, after which it runs again and gets the same 200.
  • A body that is not valid JSON returns 400 and a body missing a required field returns 422, both with a plain-text message from the framework rather than the JSON error shape; a body over 2 MB is stopped by the framework with a plain-text 413 before the 1 MB canonical check runs.
  • The monthly quota counter is incremented before validation and before the idempotency check, so a rejected request or a replayed one still counts toward the period's limit.
  • The Ruby and PHP SDKs make model_provider, model_name and model_version optional, and the Ruby README shows a type of chat.completion with object input and output; the backend requires all three context fields, string input and output, and a type of output, decision or approval.
  • SDK method names: Node client.attestations.ingest, Python client.attestations.ingest (the type is passed as attestation_type), Go client.Attestations.Ingest, Java client.attestations().ingest, Ruby client.attestations.ingest, Rust client.attestations().ingest, .NET client.Attestations.IngestAsync, PHP $client->attestations->ingest.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const result = await client.attestations.ingest({
  type: "output",
  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.",
  modelProvider: "openai",
  modelName: "gpt-4.1",
  modelVersion: "2026-04-14",
  subject: { userId: "u_4821", sessionId: "sess_9f3a", department: "legal" },
  idempotencyKey: "ct-8472-summary-1",
});
console.log(result.attestation_id, result.payload_hash);
Response · 201
{
  "attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "created_at": "2026-09-22T08:14:07Z",
  "input_hash": "a46f0ad5dfb45fcbe2c303dcb34292af2f63fd79c6ff45b52dc40703a1cc5a70",
  "output_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
  "payload_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
  "status": "accepted"
}
Response fields
attestation_iduuid

Server-generated id of the record.

created_attimestamp (ISO 8601)

When the server accepted the request; this value is part of the signed payload.

input_hashhex

SHA-256 of the UTF-8 bytes of payload.input.

output_hashhex

SHA-256 of the UTF-8 bytes of payload.output.

payload_hashhex

SHA-256 of the canonical request bytes; stored as attestation_hash and returned by the raw endpoint.

statusstring

accepted when a new record was queued, duplicate when the tenant already has a record with this payload_hash. One of accepted, duplicate.

Errors
invalid_attestation_type400

type is not output, decision or approval.

empty_payload400

payload.input or payload.output is empty or whitespace only.

invalid_context400

context.model_provider, context.model_name or context.model_version is empty or whitespace only.

subject_too_many_keys400

subject has more than 20 keys besides user_id and session_id.

subject_too_large400

subject serializes to more than 8 KB.

trace_invalid_status400

The trace exists but is in a status other than open, sealing or sealed.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

The Authorization header is present but does not use the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

insufficient_scope403

The key does not have the write scope.

payment_required402

The subscription is past due on a paid plan.

feature_not_available403

The plan's monthly AI attestations limit is 0.

quota_exceeded429

The plan's monthly AI attestations limit for the current billing period is used up.

quota_check_failed500

The quota counter could not be read; the request is rejected rather than allowed through.

trace_not_found404

trace_id was sent and no trace with that id belongs to the tenant.

trace_not_open409

trace_id points at a trace that is sealing or sealed.

idempotency_key_reuse_mismatch409

The Idempotency-Key was already used with a different body within the last 24 hours.

payload_too_large413

The canonical request bytes exceed 1 MB.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

ingest_failed500

The write queue did not acknowledge the record; any idempotency reservation is released so the same key can be retried.

serialization_failed500

The request, the queue envelope or the response could not be serialized; a reservation made for an Idempotency-Key is released when the envelope fails.

hash_error500

The computed payload_hash could not be decoded before the duplicate check.

db_error500

The database read failed.

GET/v1/ai/attestations
API keyread

List attestations

Returns a page of the tenant's active attestations, newest first, with optional date, type and provider filters.

Query parameters
pageinteger · default 1

1-based page number; values below 1 are treated as 1.

limitinteger · 1 to 500 · default 50

Rows per page; values outside the range are clamped.

date_fromtimestamp (ISO 8601)

Only records with created_at at or after this instant (inclusive).

date_totimestamp (ISO 8601)

Only records with created_at before this instant (exclusive).

attestation_typestring

Only records of this type; compared as text, so an unknown value returns an empty page. One of output, decision, approval.

model_providerstring

Only records whose context.model_provider equals this value exactly.

  • Only records with access_tier active are returned; sealed records are left out of both the rows and total.
  • Responses are cached for 30 seconds per tenant and query, so a new record can take up to 30 seconds to appear.
  • A query value that does not parse, for example a date_from that is not RFC 3339 or a negative limit, returns 400 with a plain-text body from the framework rather than the JSON error shape.
  • The scope check accepts a key with either the read scope or the write scope.
  • The Node and PHP SDK READMEs show date-only values such as 2026-07-01 for date_from and date_to; the backend parses both as RFC 3339 timestamps with an offset, so a date without a time returns 400.
  • The list rows do not carry model_version, hashes of input and output, or the signature; read one record for those.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const page = await client.attestations.list({
  limit: 50,
  attestationType: "output",
  modelProvider: "openai",
});
console.log(page.total, page.has_more, page.attestations.length);
Response · 200
{
  "attestations": [
    {
      "attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
      "attestation_type": "output",
      "attestation_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
      "model_provider": "openai",
      "model_name": "gpt-4.1",
      "retention_policy": "standard",
      "created_at": "2026-09-22T08:14:07Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 1,
  "has_more": false
}
Response fields
attestationsobject[]

The page of records, ordered by created_at descending.

attestations[].attestation_iduuid

Id of the record.

attestations[].attestation_typestring

The type sent at ingest. One of output, decision, approval.

attestations[].attestation_hashhex

SHA-256 of the canonical request bytes; the same value as payload_hash.

attestations[].model_providerstring

context.model_provider from the request; null when not stored.

attestations[].model_namestring

context.model_name from the request; null when not stored.

attestations[].retention_policystring

Retention label derived from the plan's retention days when the row was written. One of short, standard, extended, regulatory, indefinite.

attestations[].created_attimestamp (ISO 8601)

When the request was accepted.

pageinteger

The page that was returned.

limitinteger

The page size that was applied after clamping.

totalinteger

Number of active records that match the filters.

has_moreboolean

True when page * limit is below total.

Errors
missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

The Authorization header is present but does not use the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

insufficient_scope403

The key has neither the read scope nor the write scope.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

The database read failed.

GET/v1/ai/attestations/{attestation_id}
API keyread

Get an attestation

Returns one attestation with its hashes, the signed payload, the Ed25519 signature and public key, and the issuing organization.

Path parameters
attestation_iduuid · required

Id returned by ingest.

  • Decode signed_payload from hex to get compact JSON with the fields v, attestation_id, tenant_id, attestation_type, input_hash, output_hash, payload_hash, model_provider, model_name, model_version and created_at; the signature covers those bytes exactly, so verify the hex-decoded bytes, not a re-serialized copy.
  • public_key is the raw 32-byte key; libraries that want DER need the SPKI prefix 302a300506032b6570032100 in front of it, which is what the Node SDK does in verifySignature.
  • Every SDK checks the signature offline from this response: verifySignature in Node, Java and PHP, VerifySignatureAsync in .NET, VerifySignature in Go, verify_signature in Python, Ruby and Rust.
  • created_at inside signed_payload is the string the worker signed at ingest time, while the created_at field is read back from the database, which stores microseconds; real values carry fractional seconds, so check the hex-decoded bytes rather than a payload rebuilt from the other fields.
  • The response does not include the input, the output or the subject; read them with the raw endpoint and compare the SHA-256 of the returned bytes with attestation_hash.
  • A sealed record returns 410 retention_expired rather than 404 so the caller can tell the two apart.
  • The scope check accepts a key with either the read scope or the write scope.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const att = await client.attestations.get("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
console.log(att.attestation_hash, att.signature_alg, att.public_key);
Response · 200
{
  "attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "tenant_id": "3b9d6f10-52c4-4a7e-9e1b-8d0c2f4a6e71",
  "attestation_type": "output",
  "attestation_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
  "input_hash": "a46f0ad5dfb45fcbe2c303dcb34292af2f63fd79c6ff45b52dc40703a1cc5a70",
  "output_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
  "signed_payload": "7b2276223a312c226174746573746174696f6e5f6964223a2237633165346235322d396130662d346432652d623666332d326638613631633064396534222c2274656e616e745f6964223a2233623964366631302d353263342d346137652d396531622d386430633266346136653731222c226174746573746174696f6e5f74797065223a226f7574707574222c22696e7075745f68617368223a2261343666306164356466623435666362653263333033646362333432393261663266363366643739633666663435623532646334303730336131636335613730222c226f75747075745f68617368223a2261313166666565366436626139336234373866353537333138613262316138323366366532383136653062643564383137386363383839613330653430666535222c227061796c6f61645f68617368223a2263346566653135373831323134613834303436616437653035393239373763363334613563663435663463316530363136306461663736306132393561386466222c226d6f64656c5f70726f7669646572223a226f70656e6169222c226d6f64656c5f6e616d65223a226770742d342e31222c226d6f64656c5f76657273696f6e223a22323032362d30342d3134222c22637265617465645f6174223a22323032362d30392d32325430383a31343a30375a227d",
  "signature": "d98f9274a8c40c70b659e16de85937a4ea24b102aab66c143530a33564a0a75dc89d1d46c017d0ccf48b6227d5c7be54a1927bb26985219b93bc82da62ebf10a",
  "public_key": "1a3045e8b491a44ddc6b9c8a8680104945c65daff51e77e97d19ad6e9cba891f",
  "signature_alg": "ed25519",
  "model_provider": "openai",
  "model_name": "gpt-4.1",
  "model_version": "2026-04-14",
  "retention_policy": "standard",
  "created_at": "2026-09-22T08:14:07Z",
  "organization": {
    "name": "Northwind Legal",
    "issuer_name": "Northwind Legal Ltd",
    "primary_domain": "northwindlegal.example",
    "domain_verified": true,
    "domain_verified_at": "2026-05-03T10:22:41+00:00"
  }
}
Response fields
attestation_iduuid

Id of the record.

tenant_iduuid

The tenant that owns the record; also inside the signed payload.

attestation_typestring

The type sent at ingest. One of output, decision, approval.

attestation_hashhex

SHA-256 of the canonical request bytes; the same value as payload_hash.

input_hashhex

SHA-256 of payload.input; null when not stored.

output_hashhex

SHA-256 of payload.output; null when not stored.

signed_payloadhex

The exact bytes the tenant key signed: compact JSON with v, attestation_id, tenant_id, attestation_type, input_hash, output_hash, payload_hash, model_provider, model_name, model_version and created_at.

signaturehex

Ed25519 signature over signed_payload, 64 bytes.

public_keyhex

The tenant's raw 32-byte Ed25519 public key.

signature_algstring

Always ed25519. One of ed25519.

model_providerstring

context.model_provider from the request.

model_namestring

context.model_name from the request.

model_versionstring

context.model_version from the request.

retention_policystring

Retention label derived from the plan's retention days when the row was written. One of short, standard, extended, regulatory, indefinite.

created_attimestamp (ISO 8601)

When the request was accepted.

organizationobject

The issuing organization, read from the tenant's organization row when the call is made.

organization.namestring

Organization display name.

organization.issuer_namestring

Issuer name shown on proof pages.

organization.primary_domainstring

The organization's primary domain.

organization.domain_verifiedboolean

True once the primary domain passed DNS verification.

organization.domain_verified_attimestamp (ISO 8601)

When the domain was verified, written with a +00:00 offset; omitted when the domain is not verified.

organization.logo_urlstring

Logo URL set in the dashboard; omitted when not set.

Errors
invalid_path_parameter400

attestation_id is not a UUID.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

The Authorization header is present but does not use the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

insufficient_scope403

The key has neither the read scope nor the write scope.

attestation_not_found404

No attestation with this id belongs to the tenant, or the record has not been written by the worker yet.

retention_expired410

The record is past its retention window and has been sealed; a plan upgrade can unseal it.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

The database read failed.

GET/v1/ai/attestations/{attestation_id}/raw
API keyread

Get the raw payload

Returns the stored canonical request bytes for an attestation, whose SHA-256 is the attestation_hash.

Path parameters
attestation_iduuid · required

Id returned by ingest.

  • The body is the exact bytes hashed at ingest: compact JSON with no whitespace, fields in the order type, payload, context, subject, trace_id, a missing subject or trace_id written as null, and custom subject keys in sorted order.
  • SHA-256 over the response body as returned equals attestation_hash and payload_hash; hashing a pretty-printed or re-ordered copy gives a different value.
  • The response carries Content-Type application/json and Content-Disposition inline; filename="ai-attestation-raw-{attestation_id}.json".
  • The bytes are cached for 24 hours per tenant and attestation; the Node, Python and Go SDK doc comments say 5 minutes, the backend value is 86,400 seconds.
  • SDK getRaw helpers (get_raw in Python, Ruby and Rust, GetRaw in Go, GetRawAsync in .NET) parse the body into a native object; the SDK verifyPayload helpers re-serialize it compactly in the original key order before hashing, and the Rust SDK asks for the raw string because its parsed values sort keys.
  • A sealed record returns 410 retention_expired.
  • The scope check accepts a key with either the read scope or the write scope.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const raw = await client.attestations.getRaw("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
console.log(raw.type, raw.context);
Response · 200
{
  "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"
  },
  "trace_id": null
}
Response fields
typestring

The type sent at ingest. One of output, decision, approval.

payload.inputstring

The input text exactly as sent.

payload.outputstring

The output text exactly as sent.

context.model_providerstring

As sent.

context.model_namestring

As sent.

context.model_versionstring

As sent.

subjectobject

user_id and session_id when they were sent, then custom keys in sorted order; null when no subject was sent.

trace_iduuid

The trace the record was attached to; null when none was sent.

Errors
invalid_path_parameter400

attestation_id is not a UUID.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

The Authorization header is present but does not use the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

insufficient_scope403

The key has neither the read scope nor the write scope.

attestation_not_found404

No attestation with this id belongs to the tenant, or the record has not been written by the worker yet.

retention_expired410

The record is past its retention window and has been sealed; a plan upgrade can unseal it.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_query_failed500

The database read failed.

db_decode_failed500

The row is missing its storage location.

r2_fetch_failed500

The stored object could not be fetched from object storage.

r2_stream_failed500

The stored object could not be read to the end.

POST/v1/ai/attestations/{attestation_id}/verify
API keyread

Verify a hash

Compares a SHA-256 you computed against the stored attestation, input and output hashes and reports which one it matched.

Headers
Content-Typestring · required

Must be application/json; any other value is rejected by the framework with 415 before the handler runs.

Path parameters
attestation_iduuid · required

Id returned by ingest.

Request body
content_hashhex · required · exactly 64 hex characters

The SHA-256 to check: of the raw payload bytes, of payload.input or of payload.output.

  • The hash is compared against attestation_hash, then input_hash, then output_hash, then payload_hash; attestation_hash and payload_hash hold the same value, so a hash of the raw payload is reported as attestation_hash.
  • Each call consumes one API verification from the monthly quota, counted before the record is looked up, so a call for an unknown id still counts.
  • The stored hashes are cached for 60 seconds per attestation.
  • Verification does not check access_tier, so it works on sealed records that GET and the raw endpoint refuse with 410.
  • Each call is recorded as a verification event with source api; it is not shown on public proof pages.
  • There is no Idempotency-Key on this endpoint; repeated calls are separate verifications.
  • A body that is not valid JSON returns 400 and a body without content_hash returns 422, both with a plain-text message from the framework rather than the JSON error shape.
  • The SDKs compute the hash for you: verifyPayload (verify_payload in Python, Ruby and Rust, VerifyPayload in Go, VerifyPayloadAsync in .NET) takes the raw JSON and calls this endpoint, and every SDK checks that content_hash is 64 hex characters before sending.
  • Checking the Ed25519 signature does not use this endpoint; the SDK verifySignature helpers do it offline from the GET response.
Node.js
import { InvoanceClient } from "invoance";

// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();

const result = await client.attestations.verify("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4", {
  contentHash: "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
});
console.log(result.match_result, result.matched_field);
Response · 200
{
  "attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "match_result": true,
  "matched_field": "attestation_hash",
  "anchored_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
  "submitted_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
  "anchored_at": "2026-09-22T08:14:07Z",
  "organization": {
    "name": "Northwind Legal",
    "issuer_name": "Northwind Legal Ltd",
    "primary_domain": "northwindlegal.example",
    "domain_verified": true,
    "domain_verified_at": "2026-05-03T10:22:41+00:00"
  }
}
Response fields
attestation_iduuid

The id from the path.

match_resultboolean

True when content_hash equals one of the stored hashes.

matched_fieldstring

Which stored hash matched, checked in this order; null when none did. One of attestation_hash, input_hash, output_hash, payload_hash.

anchored_hashhex

The stored attestation_hash, whatever was matched.

submitted_hashhex

content_hash as received, decoded and re-encoded.

anchored_attimestamp (ISO 8601)

The record's created_at.

organizationobject

The issuing organization, read from the tenant's organization row when the call is made.

organization.namestring

Organization display name.

organization.issuer_namestring

Issuer name shown on proof pages.

organization.primary_domainstring

The organization's primary domain.

organization.domain_verifiedboolean

True once the primary domain passed DNS verification.

organization.domain_verified_attimestamp (ISO 8601)

When the domain was verified, written with a +00:00 offset; omitted when the domain is not verified.

organization.logo_urlstring

Logo URL set in the dashboard; omitted when not set.

Errors
bad_request400

content_hash is not exactly 64 hex characters.

invalid_path_parameter400

attestation_id is not a UUID.

missing_api_key401

Neither an Authorization header nor an X-API-Key header was sent.

invalid_authorization_scheme401

The Authorization header is present but does not use the Bearer scheme.

invalid_api_key_format401

The key does not start with invoance_live_.

invalid_api_key401

The key does not match any API key.

api_key_revoked401

The key has been revoked.

ip_not_allowed403

The key has an IP allowlist and the caller's address is not on it.

api_key_lookup_failed500

The key could not be looked up in the database.

insufficient_scope403

The key has neither the read scope nor the write scope.

payment_required402

The subscription is past due on a paid plan.

feature_not_available403

The plan's monthly API verifications limit is 0.

quota_exceeded429

The plan's monthly API verifications limit for the current billing period is used up.

quota_check_failed500

The quota counter could not be read; the request is rejected rather than allowed through.

attestation_not_found404

No attestation with this id belongs to the tenant, or the record has not been written by the worker yet.

rate_limited429

The tenant used up its per-second or per-minute request budget; the Retry-After header says when to retry.

db_error500

The database read failed.

Terms
input_hashSHA-256 of the UTF-8 bytes of payload.input, hex encoded.
output_hashSHA-256 of the UTF-8 bytes of payload.output, hex encoded.
payload_hashSHA-256 of the canonical request bytes, which are the request re-serialized by the server; returned by ingest and stored as attestation_hash.
attestation_hashThe record's key hash, equal to payload_hash and unique per tenant; a second request with the same bytes returns the existing record.
signed_payloadCompact JSON with v, attestation_id, tenant_id, attestation_type, input_hash, output_hash, payload_hash, model_provider, model_name, model_version and created_at; the bytes the tenant key signed.
signatureEd25519 signature over signed_payload, 64 bytes, hex encoded.
public_keyThe tenant's raw 32-byte Ed25519 public key, hex encoded, the same for every record of the tenant.
subjectOptional context about who or what triggered the call: user_id, session_id and up to 20 custom keys, all part of the hashed bytes.
retention_policyLabel derived from the plan's retention days when the row was written: short (up to 90), standard (up to 365), extended (up to 1825), regulatory (up to 2555), indefinite otherwise.
sealedA record past its retention window; it is left out of the list, get and raw return 410 retention_expired, verify still works, and a plan upgrade can unseal it.
canonical bytesThe request as the server re-serializes it: compact JSON, fields in the order type, payload, context, subject, trace_id, nulls for absent subject and trace_id, custom subject keys sorted.

Proof infrastructure. Records are hashed, signed with your organization's Ed25519 key, and stored append-only, so anyone can check them later.

Products

  • Audit Logs
  • Event Ledger
  • AI Attestation
  • Document Anchoring
  • Traces

Developers

  • Documentation
  • API reference
  • SDKs
  • How it works
  • How traces seal
  • System status

Verify

  • Audit Log
  • Event
  • AI Attestation
  • Document
  • Trace

Company

  • Company overview
  • What is Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Brand assets
  • Resources
  • Help center
  • Contact
© 2025 – 2026 Invoance, Inc. All rights reserved.© 2026 Invoance, Inc. All rights reserved.
PrivacyLegal noticeLegal FAQ