Documents SDK
The documents namespace lives under client.documents. Anchoring a document seals its SHA-256 into a signed, append-only record — optionally storing the original bytes (encrypted) for later retrieval — so anyone can later prove a file existed, unaltered, at anchor time. A key needs the write scope to anchor and the read scope to list, get, download, 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
Anchor a file
RecommendedPOST/document/anchorPass a file path or raw bytes; the SDK reads it, computes the SHA-256 hash, and uploads the original bytes (stored encrypted) for later retrieval. Set skip_original (skipOriginal in Node) to anchor the hash only without storing the file.
result = await client.documents.anchor_file(
file="./invoice.pdf",
document_ref="Invoice #1042",
event_type="invoice",
metadata={"amount": 5230, "currency": "USD"},
trace_id="9549c332-…", # optional, attach to a trace
)
print(result.event_id)Anchor a hash
POST/document/anchorIf you already have the SHA-256 hash, use the low-level method directly. Nothing leaves your machine but the digest and reference — no bytes are uploaded.
result = await client.documents.anchor(
document_hash="a94a8fe5ccb19ba61c4c0873d391e987…",
document_ref="Invoice #1042",
)
print(result.event_id)List documents
GET/documentPage through your document events, newest first. Paginated with optional filters; limit defaults to 50.
page = await client.documents.list(page=1, limit=50)
for d in page.documents:
print(d.event_id, d.document_ref)Get a document
GET/document/{event_id}Retrieve a document event by ID. has_original tells you whether the original bytes were stored at anchor time and can be downloaded.
doc = await client.documents.get("9549c332-a52b-…")
print(doc.document_ref, doc.has_original)Download the original
GET/document/{event_id}/originalRetrieve the original file bytes, if they were uploaded during anchoring. Returns raw bytes you can write straight to disk.
data = await client.documents.get_original("9549c332-a52b-…")
with open("invoice.pdf", "wb") as f:
f.write(data)Verify a document
POST/document/{event_id}/verifyCompare a hash you computed against the anchored document. match_result is true when your digest matches the sealed record.
result = await client.documents.verify(
"9549c332-a52b-…",
document_hash="a94a8fe5ccb19ba61c4c0873d391e987…",
)
print(result.match_result) # TrueErrors
Every error is a JSON body of { "error": code, "message": text }. The codes you will meet most often here: invalid_document_hash, missing_document_hash (400), idempotency_key_reuse_mismatch (409), document_not_found (404), original_not_stored (404), payload_too_large (413), and quota_exceeded (429). The full catalog, including quota and rate-limit behavior, lives in the error reference.