Home
Home/Developers/AI Attestations/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 · AI attestations

Verifying attestations

Four ways to check a record, from a public link to an offline signature check.

Attestation schemaAI Attestations API
sha-256 · 3a352297…2592
sha-256 · 58cffc7b…f059

Every attestation carries three SHA-256 hashes and an Ed25519 signature over a short signed payload. Each check below compares something you hold with one of those.

A hash match says the text or bytes you hold are the ones recorded. A signature check says the record was signed by the tenant's key. Pick the check your question needs.

Public proof pageAnyone with the link. Hashes compared in the browser; the registered key and signature shown.
Verify endpointYour API key. The server compares a hash you computed with the stored hashes.
Raw payloadYour API key. The exact stored bytes, so you can recompute payload_hash yourself.
Offline signatureEd25519 over the signed bytes with a key you fetched by the issuer's domain.
Public proofwww.invoance.com/proof/ai/{attestation_id}
  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
Public proof page

https://www.invoance.com/proof/ai/{attestation_id} opens without an account. It shows the issuer, the type, created_at, the three hashes, attestation_hash, signature_alg, the public key and the signature. The input and output are not shown.

The public key on the page is the tenant's registered key from the key table, not the key stored on the row.

The visitor pastes or drops a copy. Every check runs in the browser; nothing is uploaded.

  • Text: SHA-256 of the pasted text, compared with payload_hash, then output_hash, then input_hash.
  • Payload: SHA-256 of the exact bytes pasted or chosen as a file, compared with payload_hash.
  • Bundle: the file exported from the dashboard, checked six ways. See below.

The signature is checked only in bundle mode. Until then the page reports the hashes alone.

Behind the page: public endpoints, no key
GET/v1/proof/ai/{attestation_id}Read an AI attestation's public proofPOST/v1/proof/ai/{attestation_id}/verifyVerify an AI content hash publiclyGET/keys/{domain}Fetch an organization's public key

The public verify endpoint compares a hash with payload_hash, input_hash and output_hash. It also reports signature_valid, checked against the registered key. No quota is used.

Verify a hashFields, errors and samples

Compute the SHA-256 of the input, the output or the raw payload bytes and post it as content_hash. The reply names the stored hash it matched. One call uses one API verification from the monthly quota. The signature is not checked.

POST/v1/ai/attestations/{attestation_id}/verifyVerify a hash
Raw payloadEndpoint detail

GET /v1/ai/attestations/{attestation_id}/raw returns the stored canonical bytes, the ones hashed at ingest. Their SHA-256 is attestation_hash.

It exists so you can recompute payload_hash without rebuilding the serialization. Hash the body as returned; a pretty printed or reordered copy hashes differently.

The bytes are cached for 24 hours per attestation. A sealed record returns 410. The field order and rules are on the schema page.

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);
Dashboard bundle

The dashboard exports one attestation as ai-attestation-{attestation_id}.json. The proof page's Bundle mode reads that file.

signed_payload in the bundle is the dashboard's summary of the record, not the hex bytes the get endpoint returns. signature and public_key are standard base64 here and hex on the API.

schemaAlways invoance.ai.attestation.bundle.v1.
attestation_idThe record's id.
signed_payloadAn object: v, attestation_type, attestation_hash, input_hash, output_hash, model, model_provider, model_version, signer_type, signer_label.
payload_hashThe record's payload_hash, hex.
signatureThe record's Ed25519 signature, standard base64.
public_keyThe key stored on the record's row, standard base64.
signature_alged25519.
What the proof page checks
  • attestation_id equals the id in the proof page's URL.
  • SHA-256 of signed_payload, written as compact JSON with keys sorted, equals the bundle's payload_hash.
  • The Ed25519 signature verifies over those bytes with the bundle's public_key.
  • public_key equals the key on the proof page.
  • signature_alg equals the algorithm on the proof page.
  • payload_hash equals the payload_hash on the proof page.

All six must pass for the page to report a match.

Verification bundle
{
  "schema": "invoance.ai.attestation.bundle.v1",
  "attestation_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "signed_payload": {
    "v": 1,
    "attestation_type": "output",
    "attestation_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
    "input_hash": "a46f0ad5dfb45fcbe2c303dcb34292af2f63fd79c6ff45b52dc40703a1cc5a70",
    "output_hash": "a11ffee6d6ba93b478f557318a2b1a823f6e2816e0bd5d8178cc889a30e40fe5",
    "model": "gpt-4.1",
    "model_provider": "openai",
    "model_version": "2026-04-14",
    "signer_type": "tenant_ed25519",
    "signer_label": "tenant:3b9d6f10-52c4-4a7e-9e1b-8d0c2f4a6e71"
  },
  "payload_hash": "c4efe15781214a84046ad7e0592977c634a5cf45f4c1e06160daf760a295a8df",
  "signature": "2Y+SdKjEDHC2WeFt6Fk3pOoksQKqtmwUNTCjNWSgp13InR1GwBfQzPSLYifVx75UoZJ7smmFIZuTvILaYuvxCg==",
  "public_key": "GjBF6LSRpE3ca5yKhoAQSUXGXa/1HnfpfRmtbpy6iR8=",
  "signature_alg": "ed25519"
}
Offline signature check

Two fetches, then no more calls. The server signed the exact bytes it returns, so there is no canonicalization step on your side.

  1. Fetch the record

    GET /v1/ai/attestations/{attestation_id} with a read key. Keep signed_payload, signature and public_key. All three are hex.

  2. Fetch the issuer's key by domain

    GET https://api.invoance.com/keys/{domain}, no key needed. public_key is base64url without padding; decode it to 32 bytes. It must equal the hex decoded public_key from step 1.

  3. Take the signed bytes as returned

    Hex decode signed_payload. Do not parse it and write it again; the signature covers these bytes and no others.

  4. Verify Ed25519

    Check the 64-byte signature over the decoded bytes with the 32-byte key. Plain Ed25519, no prehash.

  5. Tie the content to the signature

    Parse the decoded bytes and read payload_hash, input_hash and output_hash. Compare them with the SHA-256 of the raw payload, the input text or the output text you hold.

The Node and Python SDK helpers do steps 1, 3 and 4 with the public_key in the same response. Step 2 is what ties the record to the issuer; the samples add it.

import { InvoanceClient } from "invoance";

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

// Fetches the record, then checks the Ed25519 signature over
// signed_payload with the public_key in that same response.
const result = await client.attestations.verifySignature("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
console.log(result.valid, result.reason);
console.log(result.signedData?.payload_hash);

// Pin the key: the issuer's registered key, fetched by domain.
const res = await fetch("https://api.invoance.com/keys/northwindlegal.example");
if (!res.ok) throw new Error("key lookup failed: " + res.status);
const key = await res.json();
const pinned = Buffer.from(key.public_key, "base64url");
const sameKey = pinned.equals(Buffer.from(result.attestation.public_key, "hex"));
console.log(result.valid && sameKey);
Sealed records

When the plan's retention window ends, the row is sealed, not deleted. Reads stop; verification keeps working.

A plan upgrade can unseal a record.

GET /v1/ai/attestations/{attestation_id}410 retention_expired
GET /v1/ai/attestations/{attestation_id}/raw410 retention_expired
GET /v1/ai/attestationsLeft out of the rows and the total
POST /v1/ai/attestations/{attestation_id}/verifyWorks. access_tier is not checked.
/proof/ai/{attestation_id} page and endpointsWork. The public handlers do not check access_tier.
Questions
Does the verify endpoint check the signature?

No. It compares hashes only. The public verify endpoint also reports signature_valid, and the SDK helpers check the signature from the get response.

Which key should I trust?

The one GET /keys/{domain}returns for the issuer's verified domain. The proof page shows that same registered key; the get endpoint returns the key stored on the row. Compare them before trusting a signature.

Does a public verification use my quota?

No. The public endpoints have no quota check. Each call is recorded as a verification event with source public_ai_url, and each proof page load as a view.

Why does get return 404 right after ingest?

The 201 means the record is on the write queue. A worker signs it and writes the row a moment later; until then get, raw and verify cannot find it.

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