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

Verifying attestations

Every attestation is signed with your tenant's Ed25519 key and written to an append-only ledger. There are five ways to check one, and they form a ladder: each level relies less on trusting our API's answer than the one before, from a quick hash comparison all the way to a public proof an auditor can open with no Invoance account.

Be precise about what each level proves. A hash match confirms content equality against what the API reports; recomputing hashes from the raw canonical bytes moves the comparison onto your machine; a signature check against a pinned tenant key ties the record to the issuing organization. Pick the level your audit actually needs.

Before you begin

The authenticated endpoints (levels 1–4) need an API key with the read scope, sent as Authorization: Bearer invoance_live_… or X-API-Key. The public proof endpoints (level 5) and the key-discovery endpoint need no auth at all. Every example is a complete request; swap invoance_live_xxx for your key.

https://api.invoance.com/v1

The verification ladder

Weakest to strongest. Levels 1 and 2 ask the API to compare; levels 3 and 4 move the math to your side; level 5 lets anyone run the check without an account, against a key we cannot swap after the fact.

1
Hash verifyAPI + SDK

Send a SHA-256 you computed; the API says whether it matches an anchored hash. Fast, but you are trusting the server's comparison.

2
Verify by payloadSDK only

The SDK hashes the content you hold client-side, then runs the same hash verify. Removes hand-rolled hashing mistakes; still trusts the server's answer.

3
Raw canonical bytesAPI + SDK

Fetch the exact bytes stored at ingest and recompute payload_hash yourself. The comparison happens on your machine.

4
Signature verifySDK only

Check the Ed25519 signature over the signed record. Against the embedded key it proves internal consistency; against a pinned key from /keys/{domain} it ties the record to the issuer.

5
Public proofNo auth

A link anyone can open and verify with no Invoance account. The server checks the signature against the write-once tenant key, never the record's own copy.

For the platform-wide picture across events, documents, and attestations, see how verification works.

1Hash verifyAPI + SDK

Compute a SHA-256 over the content you hold (the model input, the model output, or the canonical request JSON) and ask the API whether it matches one of the anchored hashes. This is the everyday check; each call counts against your plan's api_verifications_per_month quota.

POST/v1/ai/attestations/{attestation_id}/verifyVerify by hash

Body takes a single content_hash: exactly 64 hex characters (a SHA-256 digest), otherwise 400. The API compares it against attestation_hash, input_hash, output_hash, and payload_hash in that order and reports which field matched.

curl -X POST https://api.invoance.com/v1/ai/attestations/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90/verify \
  -H "Authorization: Bearer invoance_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "content_hash": "8c74176675eed4e2ff88bc0182af0f5f3a70b7e2c11d94a6b0d8f3e5c2a19b47" }'
client.attestations.verify
import hashlib

from invoance import InvoanceClient

async with InvoanceClient() as client:
    output_text = "Approved: refund of $42.00"
    result = await client.attestations.verify(
        "9549c332-a52b-4b12-8d3f-0f6c2e7a1b90",
        content_hash=hashlib.sha256(output_text.encode()).hexdigest(),
    )
    print(result.match_result, result.matched_field)  # True output_hash
FieldMeaning
match_resulttrue when the submitted hash equals one of the anchored hashes.
matched_fieldattestation_hash, input_hash, output_hash, or payload_hash; null on no match. attestation_hash equals payload_hash and is compared first, so a canonical-payload hash reports matched_field "attestation_hash".
anchored_hashThe hash recorded at ingest (the matched field's value).
anchored_atWhen the record was accepted, on our server clock.
organizationThe issuing org: name, issuer_name, primary_domain, domain_verified.
2Verify by payloadSDK only

Hand the SDK the content instead of a hash. It serializes the payload compactly, computes the SHA-256 client-side, and calls the same hash-verify endpoint. You stop worrying about hashing mistakes on your side; the comparison itself still happens on the server.

client.attestations.verify_payload / verifyPayload
# The SDK helpers are client-side sugar over the hash-verify endpoint.
# Reproduce the canonical JSON exactly (compact, server field order), hash it, verify:
HASH=$(echo -n '{"type":"output","payload":{"input":"Customer asks: can I get a refund on order #1042?","output":"Approved: refund of $42.00"},"context":{"model_provider":"openai","model_name":"gpt-4o","model_version":"2024-11-20"},"subject":{"user_id":"user_42"}}' | shasum -a 256 | cut -d' ' -f1)

curl -X POST https://api.invoance.com/v1/ai/attestations/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90/verify \
  -H "Authorization: Bearer invoance_live_xxx" \
  -H "Content-Type: application/json" \
  -d "{\"content_hash\": \"$HASH\"}"
Field order matters. The anchored payload_hashis computed over the server's canonical serialization: compact JSON (no whitespace) with fields in struct order — type, payload (input, output), context (model_provider, model_name, model_version), then subject with extra keys in alphabetical order, and trace_idif you sent one. Fields you omitted at ingest must stay omitted. If your reconstruction differs by a single byte the hash will not match — when in doubt, fetch the canonical bytes from /raw (level 3) instead of rebuilding them.
3Raw canonical bytesAPI + SDK

Fetch the byte-identical canonical JSON that was stored at ingest and hash it yourself. This is the source of truth for recomputing payload_hash: the comparison happens on your machine, so a wrong answer from the API cannot fake a match.

GET/v1/ai/attestations/{attestation_id}/rawRaw canonical JSON

Returns the exact bytes hashed and anchored at ingest, served verbatim from object storage (cached server-side for 5 minutes). Hashing the response body must reproduce payload_hash; if it does not, the record does not match what was anchored.

curl https://api.invoance.com/v1/ai/attestations/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90/raw \
  -H "Authorization: Bearer invoance_live_xxx" \
  -o attestation.json

# Hash the file exactly as served, byte for byte:
shasum -a 256 attestation.json
# -> must equal payload_hash (and attestation_hash)
client.attestations.get_raw / getRaw
raw = await client.attestations.get_raw("9549c332-a52b-4b12-8d3f-0f6c2e7a1b90")

# Round-trip through the payload verifier: the SDK re-serializes the
# dict compactly and compares the SHA-256 against the anchored hash.
result = await client.attestations.verify_payload(
    "9549c332-a52b-4b12-8d3f-0f6c2e7a1b90",
    payload=raw,
)
print(result.match_result)  # True
For a byte-exact check outside the SDKs, download with cURL and hash the file as shown above. Parsing the JSON and re-serializing it in your own runtime can change the bytes (key reordering, whitespace, unicode escaping) and break the hash even though the content is identical.
4Signature verifySDK only

Check the Ed25519 signature over the signed record. The signature covers a server-built canonical statement: the attestation id, tenant id, type, all three hashes, the model identity, and the created_at timestamp. A valid signature proves none of those fields changed since signing.

client.attestations.verify_signature / verifySignature
# 1. Fetch the attestation
ATT=$(curl -s https://api.invoance.com/v1/ai/attestations/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90 \
  -H "Authorization: Bearer invoance_live_xxx")

# 2. Extract hex fields
SIG=$(echo $ATT | jq -r .signature)
PAYLOAD=$(echo $ATT | jq -r .signed_payload)
PUBKEY=$(echo $ATT | jq -r .public_key)

# 3. Verify with openssl (Ed25519)
echo -n "$PAYLOAD" | xxd -r -p > /tmp/payload.bin
echo -n "$SIG" | xxd -r -p > /tmp/sig.bin
echo -n "302a300506032b6570032100$PUBKEY" | xxd -r -p > /tmp/pub.der

openssl pkeyutl -verify -pubin -inkey /tmp/pub.der \
  -keyform DER -in /tmp/payload.bin -sigfile /tmp/sig.bin
Know the limit of this check. The SDK helper is an online check, not offline verification: it fetches the record from our API, then verifies the signature against the public_key embedded in that same response. That proves the record is internally consistent — the signature genuinely covers the signed bytes — but the key itself came from the server. A record forged and re-signed end to end with a different key would still pass. To close that gap, pin the key: fetch the tenant's registered signing key from the public key-discovery endpoint below and require the record's key to match it before you accept the signature.
GET/keys/{domain}Pinned tenant key

Public key discovery for a verified domain: no auth, rate limited to 5 requests per second and 200 per minute per IP. Note the path is root-level (https://api.invoance.com/keys/{domain}, not under /v1) and the key is base64url without padding, while attestation records carry the same 32 bytes hex-encoded.

curl https://api.invoance.com/keys/acme.com
# Compare the record's hex public_key against the pinned base64url key:
python3 -c "import base64,sys; print(base64.urlsafe_b64encode(bytes.fromhex(sys.argv[1])).rstrip(b'=').decode())" \
  d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
# -> must equal public_key from /keys/{domain}; only then trust a valid signature
5Public proofNo auth

Every attestation has a public proof page: no login, no Invoance account, no API key. Share the link with an auditor, a regulator, or a counterparty and they can read the record and run a verification themselves. This is the strongest server-mediated answer, because the server's own signature check is pinned to the write-once tenant key.

https://invoance.com/proof/ai/{attestation_id}

The hosted page is backed by two public API endpoints under /v1/proof/ai/*, IP rate limited to 2 requests per second and 10 per minute. You can call them directly from your own verification tooling.

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

Returns the issuing organization and the full proof record: type, all three hashes, model identity, subject ids, signature, and the tenant public key. The public_key served here comes from the tenant's write-once key registry, not from the stored row.

curl https://api.invoance.com/v1/proof/ai/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90
POST/v1/proof/ai/{attestation_id}/verifyPublic verify

Submit a 64-hex SHA-256 with no auth. The server compares it against payload_hash, input_hash, and output_hash, and independently verifies the Ed25519 signature over the signed record, reporting both results. It does not count against any quota.

curl -X POST https://api.invoance.com/v1/proof/ai/9549c332-a52b-4b12-8d3f-0f6c2e7a1b90/verify \
  -H "Content-Type: application/json" \
  -d '{ "content_hash": "8c74176675eed4e2ff88bc0182af0f5f3a70b7e2c11d94a6b0d8f3e5c2a19b47" }'
Why the pinning matters: the public endpoints verify the signature against your tenant's registered key in the key registry — a write-once record — never the key stored alongside the attestation row. A forged row that was re-signed with a different key can look perfectly self-consistent (the exact case the embedded-key check in level 4 cannot catch), but it fails the pinned check here. That is why the response reports signature_valid alongside the hash match.

Sealed records: verification outlives retention

When a record's retention window ends it is sealed, not deleted: the row stays in the append-only ledger but drops out of the read endpoints. Read access returns 410 retention_expired; verification keeps working, so a proof link you shared years ago still resolves. Upgrading your plan unseals records that fall inside the new retention window.

SurfaceActive recordSealed (past retention)
GET /v1/ai/attestations/{id}200410 retention_expired
GET /v1/ai/attestations (list)ListedHidden
GET /v1/ai/attestations/{id}/raw200410 retention_expired
POST /v1/ai/attestations/{id}/verifyWorksWorks
GET/POST /v1/proof/ai/*WorksWorks

Good to know

  • Authenticated hash verify draws from your plan's api_verifications_per_month quota and returns 429 quota_exceeded when it runs out. Public proof verification is unmetered; it is IP rate limited instead (2/s, 10/min; the keys endpoint 5/s, 200/min).
  • content_hash must be exactly 64 hex characters. Anything else is 400 bad_request on the authenticated endpoint and 400 invalid_hash on the public one.
  • Unknown ids: 404 attestation_not_found (authenticated) and 404 ai_attestation_not_found (public).
  • Ingestion is asynchronous: a 201 from ingest means durably queued, not yet queryable. A verify issued in the same instant can briefly 404 until the writer persists the row.
  • attestation_hash equals payload_hash, and the authenticated verify compares in the order attestation_hash, input_hash, output_hash, payload_hash — so a canonical-payload hash reports matched_fieldof "attestation_hash". The public verify compares payload_hash, input_hash, output_hash.

Next steps

Quick start

Ingest your first attestation and read the signed record back.

Attestation schema

Request fields, limits, the three hashes, and what gets signed.

SDK reference

Every attestation method in Python and Node, including the verifiers.

Invoance

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

Subscribe to our newsletter

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

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

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

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