Home
Home/Developers/Verification
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
Docs · Verification

Verification

What Invoance hashes, what it signs with your organization's Ed25519 key, and the three ways anyone can check a record.

Open the verifierPublic endpoints
sha-256 · 3a352297…2592
sha-256 · db2a5d5c…3a70
The pipeline

Every record takes the same path. The hash is computed over canonical bytes; the signature covers a small signed record that carries the hash, so the payload stays private while the record is public.

  1. 01 · Your appPOST /v1/eventsAPI key + Idempotency-Key
  2. 02 · CanonicalizeCanonical JSONKeys sorted, stable bytes
  3. 03 · HashSHA-256payload_hash, request_hash
  4. 04 · SignEd25519Your organization's key
  5. 05 · StoreAppend-only ledgerRows cannot be deleted
What is hashed
RecordHashComputed over
Eventpayload_hashThe canonical payload. The row also stores event_hash (the payload compact, keys as sent) and request_hash (the whole ingest envelope).
Documentdocument_hashThe file bytes. You compute it; the backend never needs the file to anchor it.
AI attestationpayload_hash, input_hash, output_hashThe request as the server re-serializes it; the UTF-8 bytes of input; the UTF-8 bytes of output.
Tracecomposite_hashThe raw 32-byte hashes of every item, concatenated in timestamp order across events, documents and attestations.
Audit eventpayload_hashThe invoance.audit/1 canonical bytes of the signed object.
Canonical JSON · backend/src/helper/canonical
KeysObject keys sorted ascending by Unicode code point, at every level.
ArraysOrder kept.
ValuesPassed through as sent: strings, numbers, booleans, null.
OutputCompact JSON, no whitespace, UTF-8. Only " and \ are escaped, control characters as \b \t \n \f \r or lowercase \u00xx; everything else is written as is.
Used forEvent payloads, the payload you send to a verify endpoint, and the audit signed object.
{
  "policy_id": "pol_8472",
  "approved_by": "risk_committee",
  "decision": "approved"
}
Where a record type differs
Audit eventsThe same sort, plus: null members removed at every level; occurred_at and ingested_at rewritten as UTC with three fractional digits and Z; floats rejected; duplicate keys in the request rejected with 400; schema_id invoance.audit/1 added to the signed object.
AI attestationsNo sort. The request is re-serialized by the server with the fields in the order type, payload, context, subject, trace_id; absent subject and trace_id are written as null; custom subject keys are sorted. Hash the raw bytes from the raw endpoint, not a re-serialized copy.
DocumentsNo JSON is hashed. document_hash is the SHA-256 of the file bytes, computed by you.
What is signed

Ed25519, one key per organization

The tenant that owns your organization has one Ed25519 key. It is generated the first time the tenant signs anything and is not rotated in v1. A worker signs each record after the API has accepted it.

The signature never covers your payload. It covers the signed record on the right, which holds the hashes, so a signature check and a hash check are two separate steps.

The verify endpoints and GET /keys/{domain} use the key registered for the tenant, not the key printed on a row. GET /keys/{domain} answers once the domain is verified and the tenant has signed one record.

Signed record by type
Eventv, event_id, tenant_id, event_type, payload_hash, request_hash, ingested_at
Documentv, tenant_id, actor_type, actor_id, event_type, document_hash_hex, ts
AI attestationv, attestation_id, tenant_id, attestation_type, input_hash, output_hash, payload_hash, model_provider, model_name, model_version, created_at
Trace sealA trace.sealed ledger event whose payload holds trace_id, composite_hash, event_count and sealed_at; signed like any event
Audit eventorg_id, event_id, seq, ingested_at, action, occurred_at, actor, targets, context, metadata and schema_id invoance.audit/1
RecordSignature and key are returned byFields
EventGET /v1/proof/event/{event_id} signed_payload, signature, public_key, all hex
DocumentGET /v1/document/{event_id} signed_payload_b64, signature_b64, public_key_b64
AI attestationGET /v1/ai/attestations/{attestation_id} or the public proof signed_payload, signature, public_key, all hex
TraceGET /v1/traces/{trace_id}/proof every item's hash, signature and public key, plus seal_event
Audit eventGET /v1/audit/events/{id} payload_hash, signature, signing_public_key; the bytes are rebuilt from the row
GET/keys/{domain}
Public

Fetch an organization's public key

Resolves a verified domain to the tenant's Ed25519 public key so a verifier can check signatures without trusting the key printed on the record.

Path parameters
domainstring · required · 1 to 253 characters

The organization's primary domain; trimmed, lowercased and stripped of a trailing dot before lookup.

  • The route is mounted at the API root, https://api.invoance.com/keys/{domain}, not under /v1 or /v1/public; there is no /v1/public/keys route.
  • The key here is base64url; the proof endpoints return the same 32 bytes hex-encoded, so decode both before comparing.
  • A verifier checks a record in three steps: fetch the key by the issuer's domain, confirm the record's public_key is the same bytes, then verify the Ed25519 signature over the hex-decoded signed_payload.
  • One key per tenant, generated by the first signing operation and not rotated in v1; the key row is write-once.
  • The router attaches a limiter configured for 5 requests per second and 200 per minute, but it keys requests by a request extension nothing sets on this route, so no limit is enforced as written.
  • No SDK wraps this endpoint; the Node and Python SDKs' offline audit verification helpers accept the decoded key as their pinned public key argument.
Node.js
import { createPublicKey, verify } from "node:crypto";

const domain = "acme.com";
const eventId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";

// 1. The key for the issuer's verified domain (no API key needed).
const keyRes = await fetch("https://api.invoance.com/keys/" + domain);
if (!keyRes.ok) throw new Error("key lookup failed: " + keyRes.status);
const key = await keyRes.json();

// 2. The signed record, from the public event proof endpoint.
const proofRes = await fetch("https://api.invoance.com/v1/proof/event/" + eventId);
if (!proofRes.ok) throw new Error("proof lookup failed: " + proofRes.status);
const { event } = await proofRes.json();

// 3. Raw 32-byte Ed25519 key to SPKI DER, which node:crypto can load.
const raw = Buffer.from(key.public_key, "base64url");
const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
const pinned = createPublicKey({ key: spki, format: "der", type: "spki" });

// 4. The record must name the same key, and the signature must verify with it.
const sameKey = Buffer.from(event.public_key, "hex").equals(raw);
const signatureValid = verify(
  null,
  Buffer.from(event.signed_payload, "hex"),
  pinned,
  Buffer.from(event.signature, "hex"),
);
console.log(key.key_id, sameKey, signatureValid);
Response · 200
{
  "domain": "acme.com",
  "public_key": "1EQ72dMO9MLg5dsDRn48XHQDWEgsGh4wzbJ6LgKjgXY",
  "algorithm": "ed25519",
  "key_id": "invoance:pk_4e8a1d27"
}
Response fields
domainstring · required

The verified primary domain the key belongs to, as stored.

public_keystring · required

The 32-byte Ed25519 public key, base64url without padding (43 characters); decode it and load it as a raw key.

algorithmstring · required

Signing algorithm as stored on the key row. One of ed25519.

key_idstring · required

Display identifier: invoance:pk_ followed by the first 8 hex characters of the tenant id; for correlation, not for cryptography.

Errors
invalid_domain400

The domain is empty or longer than 253 characters after normalisation.

domain_not_found404

No organization is registered with this domain.

domain_not_verified404

An organization is registered but has not completed domain verification.

keys_not_yet_provisioned404

The domain is verified but the tenant has not signed anything yet; the key is generated on the first event, document or attestation.

db_error500

A lookup query failed.

serialization_error500

A column could not be decoded.

Three ways to verifyOpen the verifier
  1. 01 · AnyoneOpens the proof linkNo account, no dashboard access
  2. 02 · CheckRecompute the hashSubmitted payload against anchored hash
  3. 03 · Resultmatch_result: truePlus issuer domain and signing time

1. A proof page

Open /proof, pick the type, paste the ID. No account. The page shows the issuer, the hashes, the key and the signature, then checks a hash, a payload or a file you supply against the record.

  • Event
  • Document
  • AI attestation
  • Trace
  • Audit event

2. A verify endpoint

With your API key, send the hash you computed and get match_result back. The event, document and attestation forms compare hashes only; the audit form also checks the signature.

  • POST/v1/events/{event_id}/verify payload or payload_hash; matched_field names the hash that was equal
  • POST/v1/document/{event_id}/verify document_hash of the file
  • POST/v1/ai/attestations/{attestation_id}/verify content_hash, matched against the request, input and output hashes
  • GET/v1/audit/events/{id}/verify no body; rebuilds the bytes and checks hash and signature against the registered key
  • GET/v1/traces/{trace_id}/proof the bundle; recompute composite_hash from the item hashes yourself

3. Your own check

Hold the record, fetch the organization's key from GET /keys/{domain}, and verify the Ed25519 signature over the signed bytes with any library. Once you hold the record and the key, the API is not needed again.

Every SDK ships this for AI attestations (verifySignature) and audit events (verifyAuditEvent with a pinned key).

Offline verification in each SDK
Public proof endpointsAll endpoints

No API key. GET returns the public part of a record; POST takes a hash you computed and returns match_result and signature_valid. Trace and audit proofs live on their pages: GET /v1/proof/trace/{trace_id} GET /v1/proof/audit/{event_id} POST /v1/proof/audit/{event_id}/verify

GET/v1/proof/event/{event_id}
Public

Read an event's public proof

Returns the issuing organization and the public part of a signed event: hashes, algorithm, public key, signature and the signed bytes.

Path parameters
event_iduuid · required

The event_id returned by POST /v1/events.

  • The payload itself is not returned; a holder proves possession by hashing it and calling the verify endpoint.
  • public_key is read from the event row, while the verify endpoint and GET /keys/{domain} use the registered key in the tenant key table; compare the two before trusting a signature.
  • The example signature is a real Ed25519 signature over the hex-decoded signed_payload with the example key, so the samples on GET /keys/{domain} verify it as written.
  • A non-UUID event_id gets a plain-text 400 from the framework rather than the JSON error shape.
  • Each call records a view (source, IP address, user agent, country) for the issuer's dashboard.
  • Timestamps are RFC 3339 with a +00:00 offset and fractional seconds when the stored time has them.
Node.js
const eventId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";

const res = await fetch("https://api.invoance.com/v1/proof/event/" + eventId);
if (!res.ok) throw new Error("proof lookup failed: " + res.status);
const { organization, event } = await res.json();

console.log(organization.primary_domain, organization.domain_verified);
console.log(event.event_type, event.payload_hash, event.signature_alg);
Response · 200
{
  "organization": {
    "name": "Acme Corp",
    "issuer_name": "Acme Corp",
    "primary_domain": "acme.com",
    "domain_verified": true,
    "domain_verified_at": "2026-03-04T10:22:41Z",
    "logo_url": null
  },
  "event": {
    "event_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
    "event_type": "policy.approval",
    "ingested_at": "2026-09-22T08:14:07Z",
    "payload_hash": "f3c57489bbda1ac2876ce8cef9f7778e07ce2eecd5021907ad2ca2cc3895011d",
    "event_hash": "9831ee77aaae90c0b9cbf5d731c3285967ed73847d9ad7e0d8ec0a39de3c09c7",
    "signature_alg": "ed25519",
    "public_key": "d4443bd9d30ef4c2e0e5db03467e3c5c740358482c1a1e30cdb27a2e02a38176",
    "signature": "1bf6c77a72f1972861a4fe0248907eac13e3e67ed2fb6ec00470681a6542e9aa482d9ea4287f958c6985a210254381b030f1e5ba9f699482430fb80c7f44ae06",
    "signed_payload": "7b2276223a312c226576656e745f6964223a2237633165346235322d396130662d346432652d623666332d326638613631633064396534222c2274656e616e745f6964223a2234653861316432372d366233632d346639652d613264352d376330623965336631613638222c226576656e745f74797065223a22706f6c6963792e617070726f76616c222c227061796c6f61645f68617368223a2266336335373438396262646131616332383736636538636566396637373738653037636532656563643530323139303761643263613263633338393530313164222c22726571756573745f68617368223a2232623639626230346132373735373532336239373737626661653361343565633635396337396436666164663134343761613965336330376663663862633434222c22696e6765737465645f6174223a22323032362d30392d32325430383a31343a30375a227d",
    "retention_policy": "standard",
    "expires_at": null
  }
}
Response fields
organizationobject · required

The issuing organization, read from the organization row of the tenant that owns the record.

organization.namestring · required

Organization display name.

organization.issuer_namestring · required

Issuer name as snapshotted when the record was written, or the current one when no snapshot exists.

organization.primary_domainstring · required

The organization's primary domain, the value GET /keys/{domain} resolves.

organization.domain_verifiedboolean · required

True when domain ownership has been verified.

organization.domain_verified_attimestamp (ISO 8601)

When the domain was verified; null when it is not verified.

organization.logo_urlstring

Logo URL when one is set; null otherwise.

eventobject · required

The public part of the event.

event.event_iduuid · required

The event id.

event.event_typestring · required

Type given at ingest.

event.ingested_attimestamp (ISO 8601) · required

When the API accepted the event.

event.payload_hashhex · required

SHA-256 of the canonical payload (keys sorted, compact), 64 hex characters; an empty string when the row has none.

event.event_hashhex · required

SHA-256 of the payload bytes as received, 64 hex characters.

event.signature_algstring · required

Signing algorithm; ed25519 when the row records none. One of ed25519.

event.public_keyhex · required

Public key recorded on the event row, 64 hex characters; compare it with the key from GET /keys/{domain}.

event.signaturehex

Ed25519 signature over signed_payload, 128 hex characters; null when the row has none.

event.signed_payloadhex

Hex of the compact JSON that was signed: v, event_id, tenant_id, event_type, payload_hash, request_hash and ingested_at; null when the row has none.

event.retention_policystring · required

Retention policy applied to the row.

event.expires_attimestamp (ISO 8601)

When the row leaves retention; null when it does not expire.

Errors
event_not_found404

No event with that id exists.

db_error500

The lookup query failed.

POST/v1/proof/event/{event_id}/verify
Public

Verify an event hash publicly

Compares a payload hash, or a payload the server hashes, with the anchored event and reports whether the stored signature verifies against the tenant's registered key.

Headers
Content-Typestring · required

application/json

Path parameters
event_iduuid · required

The event_id returned by POST /v1/events.

Request body
payload_hashhex · exactly 64 hex characters

SHA-256 of the canonical payload; used when present, even if payload is also sent.

payloadobject

The original payload; the server sorts object keys at every depth, serializes compact JSON and hashes it with SHA-256.

  • Send one of the two fields; when both are present payload_hash is the value compared while method still reports payload.
  • Canonical form for payload: object keys sorted by byte order at every depth, arrays kept in order, no whitespace; the same form the SDKs hash client-side.
  • The signature check uses the key in the tenant key table, not the key recorded on the event row.
  • Each call is recorded as a verification for the issuer's dashboard with the outcome, IP address and country; repeated calls from one address update the same row.
Node.js
const eventId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";

const res = await fetch("https://api.invoance.com/v1/proof/event/" + eventId + "/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    payload: {
      policy_id: "pol_8472",
      approved_by: "risk_committee",
      decision: "approved",
    },
  }),
});
if (!res.ok) throw new Error("verify failed: " + res.status);
const result = await res.json();

console.log(result.match_result, result.signature_valid, result.method);
Response · 200
{
  "event_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "match_result": true,
  "signature_valid": true,
  "anchored_hash": "f3c57489bbda1ac2876ce8cef9f7778e07ce2eecd5021907ad2ca2cc3895011d",
  "submitted_hash": "f3c57489bbda1ac2876ce8cef9f7778e07ce2eecd5021907ad2ca2cc3895011d",
  "anchored_at": "2026-09-22T08:14:07Z",
  "method": "payload"
}
Response fields
event_iduuid · required

The event checked.

match_resultboolean · required

True when the submitted hash equals the anchored payload hash.

signature_validboolean · required

True when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.

anchored_hashhex · required

The payload hash stored at ingest.

submitted_hashhex · required

The hash compared, lowercased.

anchored_attimestamp (ISO 8601) · required

The event's ingested_at.

methodstring · required

payload when a payload was sent, hash otherwise. One of payload, hash.

Errors
invalid_hash400

payload_hash is not 64 hex characters.

invalid_payload400

payload could not be serialized.

missing_input400

Neither payload_hash nor payload was sent.

event_not_found404

No event with that id exists.

db_error500

A query failed.

hash_decode_failed500

The validated hash could not be decoded.

GET/v1/proof/{event_id}/organization
Public

Read a document anchor's public proof

Returns the issuing organization and the anchor time for a document anchor, identified by the event id returned when it was anchored.

Path parameters
event_iduuid · required

The event_id returned by POST /v1/document/anchor.

  • Unlike the event and AI proofs, this endpoint returns no hash, key or signature; the anchored hash is only revealed by the verify endpoint as anchored_hash.
  • The path has no resource segment: /v1/proof/{event_id}/organization is the document form, /v1/proof/event/{event_id} the event form.
  • No view is recorded for this call.
Node.js
const eventId = "3f9d2a71-5c6e-4b8a-9d1f-8e2c47b0a5d3";

const res = await fetch("https://api.invoance.com/v1/proof/" + eventId + "/organization");
if (!res.ok) throw new Error("proof lookup failed: " + res.status);
const { organization, event } = await res.json();

console.log(organization.issuer_name, organization.domain_verified);
console.log(event.event_id, event.created_at);
Response · 200
{
  "organization": {
    "name": "Acme Corp",
    "issuer_name": "Acme Corp",
    "primary_domain": "acme.com",
    "domain_verified": true,
    "domain_verified_at": "2026-03-04T10:22:41Z",
    "logo_url": null
  },
  "event": {
    "event_id": "3f9d2a71-5c6e-4b8a-9d1f-8e2c47b0a5d3",
    "created_at": "2026-09-22T08:14:07Z"
  }
}
Response fields
organizationobject · required

The issuing organization, read from the organization row of the tenant that owns the record.

organization.namestring · required

Organization display name.

organization.issuer_namestring · required

Issuer name as snapshotted when the record was written, or the current one when no snapshot exists.

organization.primary_domainstring · required

The organization's primary domain, the value GET /keys/{domain} resolves.

organization.domain_verifiedboolean · required

True when domain ownership has been verified.

organization.domain_verified_attimestamp (ISO 8601)

When the domain was verified; null when it is not verified.

organization.logo_urlstring

Logo URL when one is set; null otherwise.

eventobject · required

The anchor's identity and time.

event.event_iduuid · required

The anchor's event id.

event.created_attimestamp (ISO 8601) · required

When the document was anchored.

Errors
proof_not_found404

No document anchor with that event id exists.

db_error500

The lookup query failed.

POST/v1/proof/{event_id}/verify
Public

Verify a document hash publicly

Compares a SHA-256 the holder computed over a file with the anchored document hash and reports whether the stored signature verifies against the tenant's registered key.

Headers
Content-Typestring · required

application/json

Path parameters
event_iduuid · required

The event_id returned by POST /v1/document/anchor.

Request body
document_hashhex · required · exactly 64 hex characters

SHA-256 of the file bytes.

  • The file never leaves the holder's machine; only its SHA-256 is sent.
  • The signature check uses the key in the tenant key table; a tenant without a registered key gets signature_valid false while match_result is still computed.
  • Each call is recorded as a verification for the issuer's dashboard with the outcome, IP address and country; repeated calls from one address update the same row.
Node.js
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";

const eventId = "3f9d2a71-5c6e-4b8a-9d1f-8e2c47b0a5d3";
const documentHash = createHash("sha256").update(await readFile("./contract.pdf")).digest("hex");

const res = await fetch("https://api.invoance.com/v1/proof/" + eventId + "/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ document_hash: documentHash }),
});
if (!res.ok) throw new Error("verify failed: " + res.status);
const result = await res.json();

console.log(result.match_result, result.signature_valid, result.anchored_at);
Response · 200
{
  "event_id": "3f9d2a71-5c6e-4b8a-9d1f-8e2c47b0a5d3",
  "match_result": true,
  "signature_valid": true,
  "anchored_hash": "dd9c25fbcc4061970195f861142de4ad492bd7741f66f73cb0b1f186c676904d",
  "submitted_hash": "dd9c25fbcc4061970195f861142de4ad492bd7741f66f73cb0b1f186c676904d",
  "anchored_at": "2026-09-22T08:14:07Z"
}
Response fields
event_iduuid · required

The anchor checked.

match_resultboolean · required

True when the submitted hash equals the anchored document hash.

signature_validboolean · required

True when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.

anchored_hashhex · required

The document hash stored at anchor time.

submitted_hashhex · required

The hash compared, lowercased.

anchored_attimestamp (ISO 8601) · required

When the document was anchored.

Errors
invalid_hash400

document_hash is not 64 hex characters.

proof_not_found404

No document anchor with that event id exists.

db_error500

The lookup query failed.

hash_decode_failed500

The validated hash could not be decoded.

GET/v1/proof/ai/{attestation_id}
Public

Read an AI attestation's public proof

Returns the issuing organization and the public part of a signed AI attestation: type, model context, the three hashes, the tenant's registered key and the signature.

Path parameters
attestation_iduuid · required

The attestation_id returned by POST /v1/ai/attestations.

  • The model input and output are not returned, only their hashes; a holder proves possession through the verify endpoint.
  • public_key comes from the tenant key table through an inner join, so it is the registered key rather than whatever the attestation row records; an attestation whose tenant has no key row is reported as not found.
  • The signed bytes are not part of this response, so an offline signature check needs the authenticated GET /v1/ai/attestations/{attestation_id} instead.
  • Each call records a view (source, IP address, user agent) for the issuer's dashboard.
Node.js
const attestationId = "a1d4f8c2-7b3e-4e9a-b5c6-0d2e8f4a7c19";

const res = await fetch("https://api.invoance.com/v1/proof/ai/" + attestationId);
if (!res.ok) throw new Error("proof lookup failed: " + res.status);
const { organization, attestation } = await res.json();

console.log(organization.primary_domain, organization.domain_verified);
console.log(attestation.attestation_type, attestation.model_name, attestation.output_hash);
Response · 200
{
  "organization": {
    "name": "Acme Corp",
    "issuer_name": "Acme Corp",
    "primary_domain": "acme.com",
    "domain_verified": true,
    "domain_verified_at": "2026-03-04T10:22:41Z",
    "logo_url": null
  },
  "attestation": {
    "attestation_id": "a1d4f8c2-7b3e-4e9a-b5c6-0d2e8f4a7c19",
    "created_at": "2026-09-22T08:14:07Z",
    "attestation_type": "output",
    "input_hash": "a46f0ad5dfb45fcbe2c303dcb34292af2f63fd79c6ff45b52dc40703a1cc5a70",
    "output_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
    "payload_hash": "ab14ed66d7ac026c536531ef46c9aef9db200268289d72c6626894b767b9c885",
    "model_provider": "anthropic",
    "model_name": "claude-sonnet-4-5",
    "model_version": "2026-05-01",
    "subject_user_id": "user_4821",
    "subject_session_id": null,
    "attestation_hash": "ab14ed66d7ac026c536531ef46c9aef9db200268289d72c6626894b767b9c885",
    "signature_alg": "ed25519",
    "public_key": "d4443bd9d30ef4c2e0e5db03467e3c5c740358482c1a1e30cdb27a2e02a38176",
    "signature": "c7083f14bf12347f2b40d28016c35a4da622bfa04d11c98f44b51c58aa401d35a03cf84b06b6d8a6646a4553e4a5adff45cae350701b7f5df0a7d6279f979704",
    "retention_policy": "standard",
    "expires_at": null
  }
}
Response fields
organizationobject · required

The issuing organization, read from the organization row of the tenant that owns the record.

organization.namestring · required

Organization display name.

organization.issuer_namestring · required

Issuer name as snapshotted when the record was written, or the current one when no snapshot exists.

organization.primary_domainstring · required

The organization's primary domain, the value GET /keys/{domain} resolves.

organization.domain_verifiedboolean · required

True when domain ownership has been verified.

organization.domain_verified_attimestamp (ISO 8601)

When the domain was verified; null when it is not verified.

organization.logo_urlstring

Logo URL when one is set; null otherwise.

attestationobject · required

The public part of the attestation.

attestation.attestation_iduuid · required

The attestation id.

attestation.created_attimestamp (ISO 8601) · required

When the attestation was stored.

attestation.attestation_typestring · required

Type given at ingest.

attestation.input_hashhex · required

SHA-256 of the model input, 64 hex characters.

attestation.output_hashhex · required

SHA-256 of the model output, 64 hex characters.

attestation.payload_hashhex · required

SHA-256 of the canonical attestation request, 64 hex characters.

attestation.model_providerstring · required

Model provider given at ingest.

attestation.model_namestring · required

Model name given at ingest.

attestation.model_versionstring · required

Model version given at ingest.

attestation.subject_user_idstring

Subject user id when one was given; null otherwise.

attestation.subject_session_idstring

Subject session id when one was given; null otherwise.

attestation.attestation_hashhex · required

The hash the writer stores as the row's identity: the same bytes as payload_hash, 64 hex characters.

attestation.signature_algstring · required

Signing algorithm recorded on the row; the string unknown when missing. One of ed25519.

attestation.public_keyhex · required

The tenant's registered public key, 64 hex characters; the same bytes GET /keys/{domain} returns base64url.

attestation.signaturehex · required

Ed25519 signature over the attestation's signed bytes, 128 hex characters.

attestation.retention_policystring

Retention policy applied to the row; null when none.

attestation.expires_attimestamp (ISO 8601)

When the row leaves retention; null when it does not expire.

Errors
ai_attestation_not_found404

No attestation with that id exists, or its tenant has no registered key.

db_error500

The lookup query failed or a column could not be decoded.

POST/v1/proof/ai/{attestation_id}/verify
Public

Verify an AI content hash publicly

Compares a SHA-256 with the attestation's payload, input and output hashes, says which one matched, and reports whether the stored signature verifies against the tenant's registered key.

Headers
Content-Typestring · required

application/json

Path parameters
attestation_iduuid · required

The attestation_id returned by POST /v1/ai/attestations.

Request body
content_hashhex · required · exactly 64 hex characters

SHA-256 of the model input, the model output or the canonical attestation request.

  • The hash is compared with payload_hash, then input_hash, then output_hash; the first match wins and is reported in matched_field.
  • The signature check uses the key in the tenant key table, not the key recorded on the attestation row.
  • Each call is recorded as a verification for the issuer's dashboard; the anchored hash stored with it is always payload_hash.
  • The example output text hashes to the example output_hash, so the samples match as written.
Node.js
import { createHash } from "node:crypto";

const attestationId = "a1d4f8c2-7b3e-4e9a-b5c6-0d2e8f4a7c19";
const output = "Either party may terminate with 30 days written notice. Early termination fees do not apply after month 12.";
const contentHash = createHash("sha256").update(output).digest("hex");

const res = await fetch("https://api.invoance.com/v1/proof/ai/" + attestationId + "/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ content_hash: contentHash }),
});
if (!res.ok) throw new Error("verify failed: " + res.status);
const result = await res.json();

console.log(result.match_result, result.matched_field, result.signature_valid);
Response · 200
{
  "attestation_id": "a1d4f8c2-7b3e-4e9a-b5c6-0d2e8f4a7c19",
  "match_result": true,
  "signature_valid": true,
  "matched_field": "output_hash",
  "anchored_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
  "submitted_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
  "anchored_at": "2026-09-22T08:14:07Z"
}
Response fields
attestation_iduuid · required

The attestation checked.

match_resultboolean · required

True when the submitted hash equals payload_hash, input_hash or output_hash.

signature_validboolean · required

True when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.

matched_fieldstring

Which stored hash matched; null when none did. One of payload_hash, input_hash, output_hash.

anchored_hashhex · required

The matched stored hash, or payload_hash when nothing matched.

submitted_hashhex · required

The hash compared, lowercased.

anchored_attimestamp (ISO 8601) · required

The attestation's created_at.

Errors
invalid_hash400

content_hash is not 64 hex characters.

ai_attestation_not_found404

No attestation with that id exists.

db_error500

A query failed.

hash_decode_failed500

The validated hash could not be decoded.

Limits of a check
Does a valid signature prove the content is true?
No. It proves the bytes you hold are the bytes that were hashed and signed at that time, under that tenant's key. What the bytes say is not checked.
Can I verify when the API is unreachable?
A signature check needs only the record, its signature and the key. Keep a copy of the record and of the key from GET /keys/{domain}; the SDK helpers run offline. Hash comparison endpoints and proof pages need the API.
Does verify still answer for a record past retention?
Events, documents and attestations past retention are sealed: get and list hide them, verify still answers, and ledger rows are never deleted. Audit events are purged after retention_days.

Verified documents and records. Invoance records the exact version of every invoice, report and notice your company sends, so the people who receive them can confirm they have the version you sent, without taking your word for it.

Products

  • Evidence Rooms
  • 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