On this page
Why the API returns the signature material
When you fetch a document anchor or an AI attestation from the Invoance API, the response includes the bytes that were signed, the Ed25519 signature over them, and the public key that signed them. It does not include a field that says the record is verified.
That is deliberate. If we returned a verdict, you would be relying on us to have computed it honestly, and anyone you later showed the record to would be relying on us as well. Returning the material instead means the check can be run by whoever is holding the record, with an Ed25519 implementation of their choosing. It needs no Invoance account and no API call, and if the record is already on disk it needs no network either.
This post goes through that check end to end: what the three fields contain, how to verify the signature in Node and in Python, how to pin the key we publish for your organisation so the check is tied to you rather than to the record, and how to compare the record to the file or text you were actually handed.
What comes back with a record
A document anchor is returned by GET /v1/document/{event_id}. Alongside the document hash, the reference, the metadata and the issuing organisation, the response includes signed_payload_b64, signature_b64 and public_key_b64. All three are standard base64. The signed payload decodes to a small JSON object: a version number, the tenant id, the actor that made the call, the event type, the SHA-256 of the document as hex, and the timestamp the record was written.
An AI attestation is returned by GET /v1/ai/attestations/{attestation_id}. The same three values are there as signed_payload, signature and public_key, encoded as hex rather than base64, together with signature_alg, which is ed25519. The signed payload here is a longer canonical record: the attestation id, the tenant, the attestation type, the SHA-256 hashes of the input and the output, a hash of the full payload, the model provider, name and version, and the creation time.
In both cases the signature covers the decoded payload bytes exactly as returned. You do not need to rebuild anything or know the canonicalisation rules. Decode, verify, then read the JSON to see what was signed.
{
"v": 2,
"tenant_id": "b58e03d7-4c1f-4b8e-9a0d-2f6c7e1d41c6",
"actor_type": "api_key",
"actor_id": "7c1e4a0b-9d2e-4f3a-8b1c-0a5e6d7f9f2d",
"event_type": "document.anchored",
"document_hash_hex": "9f2c81d0c3a7e5b64d1f0a8e2b9c7d6f5e4a3b2c1d0e9f8a7b6c5d4e3f2a1b0e41ab",
"ts": "2026-10-06T10:42:07.184233Z"
}
Checking the signature in Node
Node's built-in crypto module verifies Ed25519 with no extra dependency. The only wrinkle is that Node wants the public key in SPKI DER form rather than as 32 raw bytes, so you prefix the raw key with the fixed twelve-byte header from RFC 8410 before loading it. The Invoance Node SDK does exactly this internally.
The example below takes a document anchor response and prints whether the signature holds and what was signed. PostgreSQL base64 output can include line breaks, and Node ignores whitespace when decoding, so the values can be used as returned.
import { createPublicKey, verify } from "node:crypto";
// record is the JSON body from GET /v1/document/{event_id}
const signed = Buffer.from(record.signed_payload_b64, "base64");
const signature = Buffer.from(record.signature_b64, "base64");
const rawKey = Buffer.from(record.public_key_b64, "base64"); // 32 bytes
// Node loads Ed25519 keys as SPKI DER: fixed RFC 8410 prefix + raw key.
const publicKey = createPublicKey({
key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), rawKey]),
format: "der",
type: "spki",
});
const valid = verify(null, signed, publicKey, signature);
console.log(valid ? "signature valid" : "signature INVALID");
console.log(JSON.parse(signed.toString("utf8")));
The same check in Python
PyNaCl wraps libsodium and verifies Ed25519 in two lines. It raises on a bad signature rather than returning false, so wrap the call. For an AI attestation, swap base64 decoding for bytes.fromhex and the field names for signed_payload, signature and public_key. Everything else is identical.
import base64
import json
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
# record is the JSON body from GET /v1/document/{event_id}
signed = base64.b64decode(record["signed_payload_b64"])
signature = base64.b64decode(record["signature_b64"])
public_key = base64.b64decode(record["public_key_b64"])
try:
VerifyKey(public_key).verify(signed, signature)
print("signature valid")
print(json.loads(signed))
except BadSignatureError:
print("signature INVALID")
Pin the published key, not the key in the record
A signature that verifies against the public key embedded in the same response proves internal consistency: the payload and the signature belong together. It does not yet prove that the key belongs to the organisation you think it does. Someone who could forge a whole record could forge the embedded key along with it.
The fix is to get the key from somewhere the record cannot influence. Invoance publishes every tenant's public key at a fixed address keyed by the organisation's verified domain. Fetch it once, store it, and compare it to the key in each record before you verify. If they differ, stop. If they match, the signature now ties the record to that organisation, not just to itself.
The published key is base64url without padding, so decode it accordingly before comparing bytes. Node accepts base64url directly. In Python, append padding and use urlsafe_b64decode.
curl https://api.invoance.com/v1/keys/acme.example
{
"domain": "acme.example",
"public_key": "tR9kq2mX4pL8vN1cB6zY3hW0jF7aE5dG2uS9oK4iQ8w",
"algorithm": "ed25519",
"key_id": "invoance:pk_b58e03d7"
}
Then match the record to the thing in your hand
A valid signature tells you the record is what Invoance signed at that time. The second half of any real check is whether the record describes the file or the text you were actually given.
For a document, hash the file you hold with SHA-256 and compare the hex digest to document_hash_hex inside the signed payload. On a Mac or Linux machine that is one command. If the digests match, the file is byte for byte the one that was anchored at the recorded time. If they differ, the file changed after anchoring, or it was never the anchored file at all, and you now know that with certainty rather than suspicion.
For an AI attestation, hash the prompt and the output the same way and compare to input_hash and output_hash. If you would rather not do the hashing yourself, the attestation's verify endpoint accepts the raw text and does the comparison, and the public verification page does it in a browser. Both are conveniences. The offline route is always available and needs nothing from us.
shasum -a 256 contract-final.pdf
# 9f2c81d0c3a7e5b64d1f0a8e2b9c7d6f5e4a3b2c1d0e9f8a7b6c5d4e3f2a1b0e41ab contract-final.pdf
# Compare to document_hash_hex in the signed payload. Equal means unchanged.
Why this matters when a record is challenged
Consider a counterparty producing a contract eighteen months after signing with a different liability cap. Your side anchored the executed PDF on signing day. The other side's lawyer, their expert, or the court can take your anchor record, check the signature against your published key with the code above, hash the PDF they were handed, and compare. Nobody in that room has to trust you, and nobody has to trust Invoance. The maths either holds or it does not.
The same shape applies to an AI output in a regulatory inquiry, a policy document in an employment dispute, or an invoice in a payment fraud case. In every one, the useful property is that the person asking the question can run the check themselves. A verification that depends on calling the vendor is only as good as the vendor's availability, honesty and continued existence. A verification that runs against a public key and a standard algorithm depends on none of those.
That is why Invoance returns the signed bytes, the signature and the key with every record instead of a verification result. A result is something you take our word for. The material is something you can check.
