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

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

Python
pip install invoance

# The Ed25519 signature verifier (verify_signature) additionally needs PyNaCl:
pip install pynacl

Create a client

Both SDKs read INVOANCE_API_KEY from the environment automatically. You can also pass the key explicitly.

Python
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/attestations

Seal 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.

Python
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/attestations

Page 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.

Python
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.

Python
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}/raw

Fetch 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.

Python
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}/verify

Ask 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.

Python
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}/verify

Hand 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.

Python
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).

Python
# 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.

Next steps

AI Attestations quick startAttestation schemaVerification guide
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.••