AI Attestations SDK
The attestations namespace lives under client.attestations. Each attestation seals an AI input, output, and model identity into a signed, append-only record: three SHA-256 hashes plus a per-tenant Ed25519 signature that anyone can independently verify. A key needs the write scope to ingest and the read scope to list, get, and verify.
Base URL and authentication
All endpoints live under https://api.invoance.com/v1. Authenticate every request with your API key, either as Authorization: Bearer invoance_live_... or as X-API-Key: invoance_live_....
Install
pip install invoance
# The Ed25519 signature verifier (verify_signature) additionally needs PyNaCl:
pip install pynaclCreate a client
Both SDKs read INVOANCE_API_KEY from the environment automatically. You can also pass the key explicitly.
import asyncio
from invoance import InvoanceClient
async def main() -> None:
# Reads INVOANCE_API_KEY from the environment
async with InvoanceClient() as client:
...
# Or pass it explicitly:
# async with InvoanceClient(api_key="invoance_live_...") as client:
asyncio.run(main())Methods
Create an attestation
POST/v1/ai/attestationsSeal one AI interaction. type is one of output, decision, or approval. input, output, and the three model context fields are required and non-empty. subject is an optional dict of user_id, session_id, and up to 20 extra keys (8 KB max); trace_id must reference one of your open traces. The whole body is capped at 1 MB. Persistence is asynchronous: a 201 means durably queued, so a get() immediately after may briefly 404.
res = await client.attestations.ingest(
attestation_type="output",
input="Summarize this contract for a non-lawyer.",
output="This agreement covers a 12-month engagement...",
model_provider="openai",
model_name="gpt-4o",
model_version="2024-11-20",
subject={"user_id": "user_123", "session_id": "sess_456"},
idempotency_key="att-req-8f3d2c1a", # optional
trace_id="6c9d2f4e-...", # optional, must be an open trace
)
print(res.attestation_id, res.status) # "accepted" (or "duplicate")Returns{ attestation_id, created_at, input_hash, output_hash, payload_hash, status }. status is "accepted" on first write; resubmitting a byte-identical body returns 200 with status "duplicate" and the original attestation_id. Reusing an idempotency key with a different body fails with 409 idempotency_key_reuse_mismatch.
List attestations
GET/v1/ai/attestationsPage through your attestations, newest first. Filters: date_from / date_to (RFC 3339, from inclusive, to exclusive), attestation_type (output, decision, approval), and model_provider (exact match). limit defaults to 50 and clamps to 500. Responses are served from a 30-second cache, so a just-ingested record can take a moment to appear.
page = await client.attestations.list(
page=1,
limit=100,
date_from="2026-06-01T00:00:00Z",
attestation_type="output",
model_provider="openai",
)
for att in page.attestations:
print(att.attestation_id, att.attestation_hash)
print(page.total, page.has_more)Returns{ attestations, page, limit, total, has_more }. Each item carries attestation_id, attestation_type, attestation_hash, model_provider, model_name, retention_policy, and created_at. Records sealed past their retention window are omitted.
Get an attestation
GET/v1/ai/attestations/{attestation_id}Fetch the full signed record for one attestation, including the exact bytes that were signed and the signature itself.
att = await client.attestations.get("a1b2c3d4-...")
print(att.attestation_type, att.attestation_hash)
print(att.signature_alg) # "ed25519"Returns{ attestation_id, tenant_id, attestation_type, attestation_hash, input_hash, output_hash, signed_payload (hex canonical signed bytes), signature (hex, 64-byte Ed25519), public_key (hex, 32-byte), signature_alg, model_provider, model_name, model_version, retention_policy, created_at, organization }. Records sealed past their retention window return 410 retention_expired; verification endpoints still work on sealed records.
Get the raw canonical payload
GET/v1/ai/attestations/{attestation_id}/rawFetch the exact, byte-identical canonical JSON that was hashed at ingest, straight from object storage. Hashing these bytes with SHA-256 reproduces payload_hash, so this is the ground truth for any independent recomputation. The server caches the bytes for five minutes.
raw = await client.attestations.get_raw("a1b2c3d4-...")
print(raw["payload"]["output"])
print(raw["context"]["model_name"])ReturnsThe original canonical JSON object: { type, payload: { input, output }, context: { model_provider, model_name, model_version }, subject?, trace_id? }. Sealed records return 410 retention_expired.
Verify by hash
POST/v1/ai/attestations/{attestation_id}/verifyAsk the API whether a SHA-256 you computed matches the anchored record. content_hash must be exactly 64 hex characters. The server compares it against attestation_hash, input_hash, output_hash, and payload_hash. Each call counts against your monthly verification quota.
import hashlib
output_text = "This agreement covers a 12-month engagement..."
digest = hashlib.sha256(output_text.encode()).hexdigest()
result = await client.attestations.verify("a1b2c3d4-...", content_hash=digest)
print(result.match_result, result.matched_field) # True "output_hash"Returns{ attestation_id, match_result, matched_field, anchored_hash, submitted_hash, anchored_at, organization }. matched_field is "attestation_hash", "input_hash", "output_hash", "payload_hash", or null when nothing matched.
Verify by payload
SDK onlyPOST/v1/ai/attestations/{attestation_id}/verifyHand the SDK a payload you hold locally; it serializes it compactly, computes the SHA-256 client-side, and calls verify() for you. Field-order caveat: hashing is byte-exact, so an object payload must use the server's canonical key order — type, payload, context, subject — with compact separators. The safest input is the object you get back from get_raw(), or the exact string/bytes you originally sent.
payload = await client.attestations.get_raw("a1b2c3d4-...")
result = await client.attestations.verify_payload(
"a1b2c3d4-...", payload=payload
)
print(result.match_result, result.matched_field) # True "payload_hash"ReturnsThe same shape as verify(): { attestation_id, match_result, matched_field, anchored_hash, submitted_hash, anchored_at, organization }. A payload match reports matched_field 'payload_hash'.
Verify the Ed25519 signature
SDK onlyGET/v1/ai/attestations/{attestation_id}Fetch the attestation from the API and check the Ed25519 signature over signed_payload against the public key embedded in that same response. To be clear about what this proves: the record is internally consistent, but both the bytes and the key come from the server in one response — it is not an offline check. For verification against the issuer's pinned, domain-bound key served by GET /keys/{domain}, follow the pinned-key recipe in the verification guide. In Python this method needs PyNaCl (pip install pynacl).
# Requires PyNaCl: pip install pynacl
result = await client.attestations.verify_signature("a1b2c3d4-...")
print(result.valid, result.reason) # True None
print(result.signed_data["payload_hash"])Returns{ valid, reason, attestation, signed_data }. valid is a boolean; reason explains a failure (or is null); signed_data is the parsed canonical statement that was signed — version, attestation id, tenant id, type, the three hashes, model context, and created_at.
Errors
Every error is a JSON body of { "error": code, "message": text }. The codes you will meet most often here: invalid_attestation_type, empty_payload, invalid_context (400), idempotency_key_reuse_mismatch (409), attestation_not_found (404), retention_expired (410), payload_too_large (413), and quota_exceeded (429). The full catalog, including quota and rate-limit behavior, lives in the error reference.