Verification
What Invoance hashes, what it signs with your organization's Ed25519 key, and the three ways anyone can check a record.
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.
- POST /v1/eventsAPI key + Idempotency-Key
- Canonical JSONKeys sorted, stable bytes
- SHA-256payload_hash, request_hash
- Ed25519Your organization's key
- Append-only ledgerRows cannot be deleted
| Record | Hash | Computed over |
|---|---|---|
| Event | payload_hash | The canonical payload. The row also stores event_hash (the payload compact, keys as sent) and request_hash (the whole ingest envelope). |
| Document | document_hash | The file bytes. You compute it; the backend never needs the file to anchor it. |
| AI attestation | payload_hash, input_hash, output_hash | The request as the server re-serializes it; the UTF-8 bytes of input; the UTF-8 bytes of output. |
| Trace | composite_hash | The raw 32-byte hashes of every item, concatenated in timestamp order across events, documents and attestations. |
| Audit event | payload_hash | The invoance.audit/1 canonical bytes of the signed object. |
| Keys | Object keys sorted ascending by Unicode code point, at every level. |
|---|---|
| Arrays | Order kept. |
| Values | Passed through as sent: strings, numbers, booleans, null. |
| Output | Compact 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 for | Event payloads, the payload you send to a verify endpoint, and the audit signed object. |
{
"policy_id": "pol_8472",
"approved_by": "risk_committee",
"decision": "approved"
}
| Audit events | The 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 attestations | No 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. |
| Documents | No JSON is hashed. document_hash is the SHA-256 of the file bytes, computed by you. |
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.
| Event | v, event_id, tenant_id, event_type, payload_hash, request_hash, ingested_at |
|---|---|
| Document | v, tenant_id, actor_type, actor_id, event_type, document_hash_hex, ts |
| AI attestation | v, attestation_id, tenant_id, attestation_type, input_hash, output_hash, payload_hash, model_provider, model_name, model_version, created_at |
| Trace seal | A trace.sealed ledger event whose payload holds trace_id, composite_hash, event_count and sealed_at; signed like any event |
| Audit event | org_id, event_id, seq, ingested_at, action, occurred_at, actor, targets, context, metadata and schema_id invoance.audit/1 |
| Record | Signature and key are returned by | Fields |
|---|---|---|
| Event | GET /v1/proof/event/{event_id} | signed_payload, signature, public_key, all hex |
| Document | GET /v1/document/{event_id} | signed_payload_b64, signature_b64, public_key_b64 |
| AI attestation | GET /v1/ai/attestations/{attestation_id} or the public proof | signed_payload, signature, public_key, all hex |
| Trace | GET /v1/traces/{trace_id}/proof | every item's hash, signature and public key, plus seal_event |
| Audit event | GET /v1/audit/events/{id} | payload_hash, signature, signing_public_key; the bytes are rebuilt from the row |
/keys/{domain}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.
domainThe 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.
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);
{
"domain": "acme.com",
"public_key": "1EQ72dMO9MLg5dsDRn48XHQDWEgsGh4wzbJ6LgKjgXY",
"algorithm": "ed25519",
"key_id": "invoance:pk_4e8a1d27"
}
domainThe verified primary domain the key belongs to, as stored.
public_keyThe 32-byte Ed25519 public key, base64url without padding (43 characters); decode it and load it as a raw key.
algorithmSigning algorithm as stored on the key row. One of ed25519.
key_idDisplay identifier: invoance:pk_ followed by the first 8 hex characters of the tenant id; for correlation, not for cryptography.
invalid_domainThe domain is empty or longer than 253 characters after normalisation.
domain_not_foundNo organization is registered with this domain.
domain_not_verifiedAn organization is registered but has not completed domain verification.
keys_not_yet_provisionedThe domain is verified but the tenant has not signed anything yet; the key is generated on the first event, document or attestation.
db_errorA lookup query failed.
serialization_errorA column could not be decoded.
- Opens the proof linkNo account, no dashboard access
- Recompute the hashSubmitted payload against anchored hash
- match_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.
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.
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 SDKNo 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
/v1/proof/event/{event_id}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.
event_idThe 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.
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);
{
"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
}
}
organizationThe issuing organization, read from the organization row of the tenant that owns the record.
organization.nameOrganization display name.
organization.issuer_nameIssuer name as snapshotted when the record was written, or the current one when no snapshot exists.
organization.primary_domainThe organization's primary domain, the value GET /keys/{domain} resolves.
organization.domain_verifiedTrue when domain ownership has been verified.
organization.domain_verified_atWhen the domain was verified; null when it is not verified.
organization.logo_urlLogo URL when one is set; null otherwise.
eventThe public part of the event.
event.event_idThe event id.
event.event_typeType given at ingest.
event.ingested_atWhen the API accepted the event.
event.payload_hashSHA-256 of the canonical payload (keys sorted, compact), 64 hex characters; an empty string when the row has none.
event.event_hashSHA-256 of the payload bytes as received, 64 hex characters.
event.signature_algSigning algorithm; ed25519 when the row records none. One of ed25519.
event.public_keyPublic key recorded on the event row, 64 hex characters; compare it with the key from GET /keys/{domain}.
event.signatureEd25519 signature over signed_payload, 128 hex characters; null when the row has none.
event.signed_payloadHex 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_policyRetention policy applied to the row.
event.expires_atWhen the row leaves retention; null when it does not expire.
event_not_foundNo event with that id exists.
db_errorThe lookup query failed.
/v1/proof/event/{event_id}/verifyVerify 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.
Content-Typeapplication/json
event_idThe event_id returned by POST /v1/events.
payload_hashSHA-256 of the canonical payload; used when present, even if payload is also sent.
payloadThe 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.
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);
{
"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"
}
event_idThe event checked.
match_resultTrue when the submitted hash equals the anchored payload hash.
signature_validTrue when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.
anchored_hashThe payload hash stored at ingest.
submitted_hashThe hash compared, lowercased.
anchored_atThe event's ingested_at.
methodpayload when a payload was sent, hash otherwise. One of payload, hash.
invalid_hashpayload_hash is not 64 hex characters.
invalid_payloadpayload could not be serialized.
missing_inputNeither payload_hash nor payload was sent.
event_not_foundNo event with that id exists.
db_errorA query failed.
hash_decode_failedThe validated hash could not be decoded.
/v1/proof/{event_id}/organizationRead 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.
event_idThe 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.
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);
{
"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"
}
}
organizationThe issuing organization, read from the organization row of the tenant that owns the record.
organization.nameOrganization display name.
organization.issuer_nameIssuer name as snapshotted when the record was written, or the current one when no snapshot exists.
organization.primary_domainThe organization's primary domain, the value GET /keys/{domain} resolves.
organization.domain_verifiedTrue when domain ownership has been verified.
organization.domain_verified_atWhen the domain was verified; null when it is not verified.
organization.logo_urlLogo URL when one is set; null otherwise.
eventThe anchor's identity and time.
event.event_idThe anchor's event id.
event.created_atWhen the document was anchored.
proof_not_foundNo document anchor with that event id exists.
db_errorThe lookup query failed.
/v1/proof/{event_id}/verifyVerify 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.
Content-Typeapplication/json
event_idThe event_id returned by POST /v1/document/anchor.
document_hashSHA-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.
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);
{
"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"
}
event_idThe anchor checked.
match_resultTrue when the submitted hash equals the anchored document hash.
signature_validTrue when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.
anchored_hashThe document hash stored at anchor time.
submitted_hashThe hash compared, lowercased.
anchored_atWhen the document was anchored.
invalid_hashdocument_hash is not 64 hex characters.
proof_not_foundNo document anchor with that event id exists.
db_errorThe lookup query failed.
hash_decode_failedThe validated hash could not be decoded.
/v1/proof/ai/{attestation_id}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.
attestation_idThe 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.
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);
{
"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
}
}
organizationThe issuing organization, read from the organization row of the tenant that owns the record.
organization.nameOrganization display name.
organization.issuer_nameIssuer name as snapshotted when the record was written, or the current one when no snapshot exists.
organization.primary_domainThe organization's primary domain, the value GET /keys/{domain} resolves.
organization.domain_verifiedTrue when domain ownership has been verified.
organization.domain_verified_atWhen the domain was verified; null when it is not verified.
organization.logo_urlLogo URL when one is set; null otherwise.
attestationThe public part of the attestation.
attestation.attestation_idThe attestation id.
attestation.created_atWhen the attestation was stored.
attestation.attestation_typeType given at ingest.
attestation.input_hashSHA-256 of the model input, 64 hex characters.
attestation.output_hashSHA-256 of the model output, 64 hex characters.
attestation.payload_hashSHA-256 of the canonical attestation request, 64 hex characters.
attestation.model_providerModel provider given at ingest.
attestation.model_nameModel name given at ingest.
attestation.model_versionModel version given at ingest.
attestation.subject_user_idSubject user id when one was given; null otherwise.
attestation.subject_session_idSubject session id when one was given; null otherwise.
attestation.attestation_hashThe hash the writer stores as the row's identity: the same bytes as payload_hash, 64 hex characters.
attestation.signature_algSigning algorithm recorded on the row; the string unknown when missing. One of ed25519.
attestation.public_keyThe tenant's registered public key, 64 hex characters; the same bytes GET /keys/{domain} returns base64url.
attestation.signatureEd25519 signature over the attestation's signed bytes, 128 hex characters.
attestation.retention_policyRetention policy applied to the row; null when none.
attestation.expires_atWhen the row leaves retention; null when it does not expire.
ai_attestation_not_foundNo attestation with that id exists, or its tenant has no registered key.
db_errorThe lookup query failed or a column could not be decoded.
/v1/proof/ai/{attestation_id}/verifyVerify 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.
Content-Typeapplication/json
attestation_idThe attestation_id returned by POST /v1/ai/attestations.
content_hashSHA-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.
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);
{
"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"
}
attestation_idThe attestation checked.
match_resultTrue when the submitted hash equals payload_hash, input_hash or output_hash.
signature_validTrue when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.
matched_fieldWhich stored hash matched; null when none did. One of payload_hash, input_hash, output_hash.
anchored_hashThe matched stored hash, or payload_hash when nothing matched.
submitted_hashThe hash compared, lowercased.
anchored_atThe attestation's created_at.
invalid_hashcontent_hash is not 64 hex characters.
ai_attestation_not_foundNo attestation with that id exists.
db_errorA query failed.
hash_decode_failedThe validated hash could not be decoded.