API Reference

Audit & Provenance API

Verify a document's signed history, bind an answer to the exact sources your application used, and optionally share a limited public receipt.

Grounding receipts are rolling out

Before production use, confirm that your hosted API exposes POST /v1/audit/grounding and that your installed SDK version includes the grounding-receipt methods below. Signed lineage is already available independently of this rollout.

Choose the right operation

NeedSDK operationREST route
Inspect one document's signed mutation historylineage / lineage / GetLineageAsync / LineageGET /v1/audit/records/{doc_id}
Bind an answer to ordered source documentscreate_grounding_receipt / createGroundingReceipt / CreateGroundingReceiptAsync / CreateGroundingReceiptPOST /v1/audit/grounding
Revoke a public receiptrevoke_grounding_receipt / revokeGroundingReceipt / RevokeGroundingReceiptAsync / RevokeGroundingReceiptDELETE /v1/audit/receipts/{receipt_id}

All /v1/audit/* routes require an API key. The public receipt and badge links described below are the deliberate exception: they are bearer capabilities, so anyone who has the link can fetch its limited aggregate proof.

Signed document lineage

Every document mutation is recorded as an Ed25519-signed provenance event: insert, update, tombstone, restore, re-embed, and GDPR hard-delete. Use lineage when you need that retained history, returned oldest-first in causal order.

records = client.lineage(document_id)
for record in records:
    print(record.action, record.proof.verified)

Lineage is scoped to the tenant in your API key. A document that is missing or belongs to another tenant returns the same 404, so the route cannot be used to probe another tenant's IDs.

Lineage response and verification

Ledger records use one envelope and carry a proof object:

FieldDescription
atRFC 3339 event timestamp.
actorSigning node (node:<hex>).
actiondocument.inserted, document.updated, document.tombstoned, document.restored, document.reembedded, or document.hard_deleted.
resourceThe affected document:<uuid>.
outcomecommitted.
sourceledger.
proof.content_idBLAKE3 content ID on content-bearing events; absent for events such as tombstones.
proof.lamportCausal position in the ledger.
proof.node_idHex signer ID. It must equal BLAKE3(public_key).
proof.public_keyHex Ed25519 public key (32 bytes).
proof.signatureHex Ed25519 signature over the ledger event.
proof.verifiedWhether Aether re-verified the signature while reading the record.

The signer identity is bound to its key: node_id == BLAKE3(public_key). A changed event invalidates its signature. Two checks make the trail independently inspectable:

  1. Aether re-verifies every retained event on each lineage read. Treat any proof.verified: false record as evidence that the retained trail is not intact.
  2. Your verifier can recompute the node ID and reject public keys outside the allowed node-key set you pin for the deployment. A self-consistent but unexpected key is not a trusted signer.

The verification result says whether the retained event is authentic; it does not make a claim about the truth of the document's contents.

Fully offline Ed25519 verification of a lineage event is not yet available from this response alone. The original signature covers canonical ledger bytes that also contain non-public storage fields, so Aether does not export that payload. Today the engine performs the signature check on every read and exposes the content ID, key identity, and result; a future redaction-safe canonical payload can make the detached signature independently reproducible without disclosing encryption material.

Direct REST requests use the same route:

Bash
curl -s https://api.aetherdb.ai/v1/audit/records/$DOC_ID \
  -H "Authorization: Bearer $AETHER_API_KEY" | jq

Bind an answer to its grounding

Call this after your application has chosen the exact documents it supplied to a model. Keep the IDs in the same order: order is part of the commitment.

scoped = client.partition("customer-acme")
proof = scoped.create_grounding_receipt(
    "The renewal is due on September 30.",
    [contract_doc_id, renewal_policy_doc_id],
)

assert proof.trust.status == "verified"
print(proof.answer_digest)

The authenticated result includes:

  • answer_digest: a BLAKE3 digest of the answer. Aether does not retain the answer for this feature.
  • sources: the private document IDs and source-version CIDs you declared. Each verified source also carries its matching engine-checked lineage proof; that proof is traceability data, not a standalone ledger signing payload.
  • trust: verified when every declared current CID had retained, valid signed evidence at issuance; otherwise partial.
  • binding: a cryptographic commitment to the answer digest and the ordered source versions. Keep the authenticated verification_salt with your application result if you need to recompute it later.
  • attestation: an Ed25519 signature over the answer digest, binding commitments, ordered-set verification result, and issue time. This compact attestation is the independently verifiable proof for the complete grounding set, including when no public link is created.

This is an integrity signal, not a truth score. It does not prove that a model reasoned correctly from a source or that an answer is factually correct.

For each source, retained_signed_event_count counts every retained event that has a valid signature from a trusted provenance signer, including lifecycle events as well as content-bearing events. Verification fails closed to partial when a later invalid or untrusted event could supersede the current content proof. A valid restore or re-embed can make a soft-tombstoned source live again; a hard-delete is irreversible and cannot be rehabilitated by a later event.

Partition and access boundaries

Use a partition-scoped client for a multi-tenant key. The SDK sends that partition automatically for both creation and revocation. Every declared source must belong to the same tenant and partition and be readable by the calling key. Missing, foreign, wrong-partition, tombstoned, and unreadable IDs intentionally have the same 404 response.

Verify the commitments and signatures

Always pin the expected signer NodeIds from deployment configuration or another trusted channel. A row-supplied public key is not a trust root, even when its signature is internally consistent. For each attestation, reject it unless signer_node_id is pinned and equals the lowercase hex BLAKE3 digest of the raw 32-byte Ed25519 public key.

The prototype does not yet publish an authoritative hosted key-history endpoint. Hosted pilot users must obtain signer pins from the Aether operator through an authenticated channel; self-hosted operators must distribute the same fleet pin set separately from receipts. For normal rotation, publish and configure a new pin before it signs, remove the old private key from service, and keep the old public NodeId pinned on receipt-serving nodes for at least the longest unexpired receipt lifetime (and in offline verifier policy for as long as historical attestations must remain verifiable). Removing a pin deliberately makes still-signed receipts from that node fail live verification. A compromised-key rotation needs an explicit revocation policy rather than normal overlap.

The authenticated binding uses these exact bytes:

  1. Encode the answer as UTF-8 without trimming or Unicode normalization. answer_hash is the raw 32-byte BLAKE3 digest; the returned answer_digest is blake3: plus its lowercase hex form.
  2. Start the source transcript with the bytes aether-grounding-source-set/v1, followed by one zero byte and the source count as an unsigned 64-bit big-endian integer. For each source in response order, append its rank as unsigned 64-bit big-endian, then document_id and content_id as UTF-8 fields. Each field is framed by its byte length as unsigned 64-bit big-endian.
  3. BLAKE3-hash that transcript. source_set_commitment is blake3: plus the lowercase hex digest.
  4. Build a second transcript beginning with aether-grounding-source-evidence/v1, one zero byte, and the source count as unsigned 64-bit big-endian. For each source, append its rank, length-framed document ID and CID, retained-event count as unsigned 64-bit big-endian, and one byte for current_content_verified (00 or 01). Append one proof-presence byte. A present proof then appends a content-ID presence byte and optional length-framed CID, Lamport value as unsigned 64-bit big-endian, length-framed lowercase-hex NodeId, public key, and signature, then one proof-verification byte. source_evidence_commitment is blake3: plus the lowercase hex BLAKE3 digest of this transcript. This separately binds every returned diagnostic proof field without changing the answer/source identity commitment.
  5. Start the binding transcript with aether-grounding-binding/v1, one zero byte, then the length-framed raw answer_hash and raw source-set hash. Decode verification_salt as canonical unpadded base64url to obtain the 32-byte BLAKE3 key. binding_commitment is the lowercase hex keyed-BLAKE3 digest of the transcript, with no blake3: prefix.

The grounding-set signature is Ed25519 over one compact UTF-8 JSON object with no insignificant whitespace. Preserve this field order exactly:

JSON
{"version":"aether-grounding-set-attestation/v1","issued_at":"<response value>","answer_digest":"blake3:<hex>","binding_algorithm":"blake3-keyed/aether-grounding-binding/v1","source_set_commitment":"blake3:<hex>","source_evidence_commitment":"blake3:<hex>","binding_commitment":"<hex>","answer_bound":true,"source_count":2,"verified_source_ranks":[0,1],"verified_source_count":2,"status":"verified"}

Derive verified_source_ranks from sources whose current_content_verified is true; derive the counts and status instead of trusting the returned aggregates. A verified source must carry a proof for the same CID whose node_id equals BLAKE3 of its public key and is also in your trusted signer set. The redacted proof still cannot reproduce the original ledger-event signature, but its complete returned representation is protected by source_evidence_commitment. Verify the aggregate signature over the reconstructed object with the pinned Ed25519 key. The response's attestation.verified flag reports the engine's check but does not replace yours.

The runnable Python verifier implements this byte-for-byte protocol and also checks an optional public receipt. Full mode takes the original answer plus authenticated response and cross-links the private binding to the public receipt. Public-only mode takes just the fetched receipt JSON, capability, and signer pin; it verifies the aggregate structure, expiry, capability binding, and signature while correctly treating the answer/source commitment as opaque. It requires blake3 and cryptography and takes signer pins only from --trusted-node-id, never from the receipt bundle being verified.

Bash
curl --fail --proto '=https' "$RECEIPT_URL" -o receipt.json
python verify-grounding-receipt.py receipt.json \
  --share-token "$RECEIPT_CAPABILITY" \
  --trusted-node-id "$AETHER_SIGNER_NODE_ID"

To reproduce every canonical transcript and signature without contacting a live deployment, download the golden verification bundle beside the verifier and run:

Bash
python verify-grounding-receipt.py grounding-receipt-test-vector.json \
  --trusted-node-id 0871f3aabc26e4582c508af5c03884e6a96f0989d1dd8cfb49cd17ed25792433 \
  --allow-expired

--allow-expired is appropriate only for this fixed historical vector. Live receipts must pass the default current-time expiry check and a fresh capability fetch to establish revocation status.

Share a public receipt

Sharing is explicit. Pass share=True (Python), { share: true } (TypeScript), share: true (.NET), or true (Go) when you create the grounding proof.

The SDKs make a shareable creation request only once rather than automatically replaying it after a timeout or transient server response. The route does not yet promise durable idempotency, and a replay would mint another capability. If a response is lost after the server commits, that unseen link can remain valid until its 30-day expiry; a manual retry creates a separate receipt. Keep each returned receipt ID so you can revoke any superseded link.

TypeScript
const proof = await client
  .partition("customer-acme")
  .createGroundingReceipt(answer, sourceDocIds, { share: true })

const receipt = proof.receipt
// Save these URLs only where you intentionally want a public aggregate proof.
console.log(receipt?.share_url, receipt?.badge_url)

The receipt contains its ID, issue and expiry timestamps, aggregate source counts, status, opaque binding and owner commitments, and an Ed25519 attestation. The owner commitment is keyed by a separate random per-receipt value retained only in the private control plane; a public link holder cannot use it to test tenant or partition guesses. It lets the authenticated revoke path detect owner-field reassignment without publishing the owner. The receipt does not disclose the answer, answer digest, tenant, partition, document IDs, CIDs, source titles, passages, ACL data, or raw ledger events.

The signature and commitments detect a modified or reassigned control-plane row; they do not turn the receipt database into a Byzantine-available archive. An operator with control of that database can still withhold or delete a row, and restoring a previously revoked but still-unexpired valid row can make its capability URL live again. A previously copied signed response can also be replayed until its signed expiry. Use a fresh capability fetch for the database's current availability and revocation state, and retain the signed bundle separately when historical evidence matters.

To verify a public receipt, require receipt_id to be a canonical lowercase RFC 4122 version-4 UUID. Decode its 43-character capability as canonical unpadded base64url and require exactly 32 bytes. Its capability_commitment must be blake3: plus the lowercase hex BLAKE3 digest of those raw bytes. Verify Ed25519 over compact UTF-8 JSON with this exact field order:

JSON
{"version":"aether-grounding-receipt/v2","receipt_id":"<uuid>","issued_at":"<response value>","expires_at":"<response value>","source_count":2,"verified_source_count":2,"status":"verified","binding_commitment":"<hex>","capability_commitment":"blake3:<hex>","owner_commitment":"<hex>"}

In full private mode, require the public and authenticated binding_commitment, source counts, and status to match. A public-only recipient can verify that those opaque aggregate values were signed but cannot recover or inspect the private answer/source set. In both modes, derive and pin the signer identity instead of trusting attestation.verified by itself.

Also require issued_at to be no more than five minutes in the future, expires_at to be exactly 30 days later, and expires_at to be later than the current time. The five-minute allowance covers bounded fleet clock skew without letting a future-dated signature extend the link's effective lifetime. Offline signature verification cannot prove that a still-unexpired receipt has not since been revoked; establish live status with a fresh successful GET of the capability URL. The verifier rejects expired receipts by default and reserves --allow-expired for reproducing a historical golden test vector.

share_url and badge_url are paths relative to the Aether API origin. Resolve them against the same trusted base URL you configured for the SDK (the hosted default is https://api.aetherdb.ai) before publishing them. Do not derive the origin from an untrusted request Host header.

Receipt links expire 30 days after issue. Public responses are no-store and no-index, so revocation and expiry take effect on the next fetch. The badge uses only fixed status text and aggregate counts, so it is safe to embed:

Deleting or tombstoning a source document, deleting its partition, or offboarding an application does not implicitly revoke an already-issued aggregate receipt. The signed receipt describes verification at issuance and contains no source identifiers or content, so it remains fetchable until you explicitly revoke its receipt ID or its fixed 30-day lifetime expires. Revoke published links as part of your own source-deletion and offboarding workflow.

HTML
<a href="https://api.aetherdb.ai/receipts/<capability>">
  <img
    src="https://api.aetherdb.ai/receipts/<capability>/badge.svg"
    alt="Aether grounding receipt"
  />
</a>

Revoke a receipt

Revoke a link when you no longer want its aggregate proof to be public. A scoped SDK handle supplies the required partition guard.

receipt_id = proof.receipt.receipt_id
client.partition("customer-acme").revoke_grounding_receipt(receipt_id)

After a successful revoke, both /receipts/{capability} and /receipts/{capability}/badge.svg return 404. Unknown, foreign, and wrong-partition receipt IDs also return 404.

REST details

Use the SDKs for application code. If you are building a custom client, create a grounding proof with:

Bash
curl -X POST https://api.aetherdb.ai/v1/audit/grounding \
  -H "Authorization: Bearer $AETHER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "answer": "The renewal is due on September 30.",
    "source_doc_ids": ["<contract-doc-id>", "<policy-doc-id>"],
    "partition": "customer-acme",
    "share": true
  }'

Public receipts are intentionally unversioned (/receipts/...), while all authenticated audit routes use /v1/audit/.... Do not treat /v1/receipts/... as a public-link alias.

Lineage retention limits

  • Retained history. Lineage covers the signed ledger events the node currently retains. Events folded into a checkpoint and truncated before a restart may not appear; the document's current state survives. Do not describe this API as an immutable, unbounded historical archive.
  • Hard-deleted documents. A GDPR hard-delete shreds the document's encryption key and purges its content. The signed deletion event remains in retained ledger history so the owning tenant can inspect evidence that the deletion occurred.