Traces
Group events, anchored documents and AI attestations into one sealed bundle with a composite hash.
A trace groups the events, anchored documents and AI attestations that belong to one process, such as an invoice run or an onboarding case. You create it open, attach items by passing its trace_id when you ingest an event, anchor a document or ingest an attestation, and seal it when the process is complete. Each item keeps its own hash and signature; the trace adds an ordered bundle on top.
Sealing runs in the background. The worker reads the SHA-256 hash of every item in the trace (payload_hash for events, document_hash for documents, payload_hash for attestations), orders them by their timestamps across all three types, and computes the composite hash as SHA-256 over the concatenated raw hash bytes. It then writes a trace.sealed event to the ledger, signed with the tenant's Ed25519 key, and stores the composite hash, the seal event id and the item counts on the trace. A sealed trace accepts no more items and cannot be deleted.
Once sealed, the proof endpoints return the trace with every item's hash, signature and public key plus the seal event, so anyone holding the bundle can recompute the composite hash and check the signatures against the tenant's public key. The owner can also make the trace visible on the public proof endpoint, which needs no API key.
/v1/tracesCreate a traceGET/v1/tracesList tracesGET/v1/traces/{trace_id}Get a traceDELETE/v1/traces/{trace_id}Delete an empty tracePOST/v1/traces/{trace_id}/sealSeal a traceGET/v1/traces/{trace_id}/proofGet the proof bundleGET/v1/traces/{trace_id}/proof/pdfDownload the proof bundle as PDFGET/v1/proof/trace/{trace_id}Get the public proof| label | 1 to 255 bytes (UTF-8) after trimming |
|---|---|
| metadata | up to 16 KB (16384 bytes) serialized |
| open traces per label | one per tenant; a second create with the same label answers 409 duplicate_trace |
| traces per month | the plan's traces_per_month quota, checked on create |
| list page size | 1 to 100, default 25 |
| event page size on get | 1 to 200, default 50 |
| seal | requires at least one event on the trace |
| delete | only an open trace with no events |
| public proof rate | a 2 per second, 10 per minute limiter is configured on the proof routes but keys on a request extension nothing sets, so it does not reject requests |
/v1/tracesCreate a trace
Creates an open trace with a label and optional metadata and returns its id; items join it later by carrying its trace_id.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
Content-TypeMust be application/json; any other value is rejected by the framework with a plain-text 415 before the handler runs.
labelA name for the trace; leading and trailing whitespace is removed before validation and the length is checked in UTF-8 bytes.
metadataAny JSON value stored with the trace and returned by GET /v1/traces/{trace_id}; not part of the composite hash.
- An item joins a trace by carrying trace_id in its own create call: POST /v1/events, POST /v1/document/anchor and POST /v1/ai/attestations each accept an optional trace_id.
- Those calls check that the trace belongs to the caller's tenant and is open; otherwise they answer 404 trace_not_found, 409 trace_sealed or 409 trace_sealing (events), or 409 trace_not_open (documents and attestations).
- In the SDKs the ingest parameter is traceId (Node, Java, PHP), TraceID (Go), TraceId (.NET) or trace_id (Python, Ruby, Rust).
- The trace quota counter is incremented before the duplicate-label check, so a request rejected with duplicate_trace still counts against traces_per_month.
- The label is unique only among open traces; once a trace is sealed a new open trace may reuse its label.
- This endpoint does not read an Idempotency-Key header; retrying a successful create with the same label returns 409 duplicate_trace.
- A body that is not valid JSON or lacks label is rejected by the framework before the handler runs with a plain-text 400 or 422 rather than the JSON error shape.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const trace = await client.traces.create({
label: "Invoice batch 2026-09",
metadata: { batch_id: "b_4471", region: "eu-west" },
});
console.log(trace.trace_id, trace.status);
{
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"status": "open",
"created_at": "2026-09-22T08:14:07Z",
"label": "Invoice batch 2026-09"
}
trace_idThe new trace's id; pass it as trace_id when ingesting items.
statusAlways open on creation. One of open.
created_atWhen the trace was created.
labelThe trimmed label as stored.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key does not have the write scope.
invalid_labellabel is empty after trimming or longer than 255 bytes; a body with no label at all is rejected by the framework first.
metadata_too_largemetadata serializes to more than 16,384 bytes of compact JSON.
payment_requiredThe organization is on a paid plan and its subscription is past due.
feature_not_availableThe plan's traces_per_month limit is 0.
quota_exceededThe monthly traces_per_month quota is used up for the current billing period.
quota_check_failedThe plan limit or usage counter could not be read.
duplicate_traceAn open trace with the same label already exists in the tenant.
trace_create_failedThe duplicate check or the insert failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/tracesList traces
Returns a page of the tenant's traces, newest first, optionally filtered by status.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
page1-based page number; values below 1 are treated as 1.
limitTraces per page; values outside the range are clamped.
statusReturn only traces with this status. One of open, sealing, sealed.
- Results are ordered by created_at descending.
- A key with the write scope passes the read check.
- The SDK parameter types for status list only open and sealed; the backend also accepts sealing.
- The SDK list-item models declare a composite_hash field that this endpoint does not return, and omit document_count, attestation_count and source, which it does return; read composite_hash from GET /v1/traces/{trace_id}.
- A page or limit value that is not a whole number is rejected by the framework with a plain-text 400 before the handler runs.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const page = await client.traces.list({ status: "open", page: 1, limit: 25 });
for (const trace of page.traces) {
console.log(trace.trace_id, trace.label, trace.status);
}
console.log(page.total, page.has_more);
{
"traces": [
{
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"label": "Invoice batch 2026-09",
"status": "sealed",
"event_count": 4,
"document_count": 1,
"attestation_count": 1,
"created_at": "2026-09-22T08:14:07Z",
"sealed_at": "2026-09-22T09:01:54Z",
"source": "api"
},
{
"trace_id": "0d2b6f4a-8c1e-4b3d-9a5f-6e7c8d9a0b1c",
"label": "Invoice batch 2026-08",
"status": "open",
"event_count": 2,
"document_count": 0,
"attestation_count": 0,
"created_at": "2026-08-25T14:02:19Z",
"sealed_at": null,
"source": "dashboard"
}
],
"page": 1,
"limit": 25,
"total": 2,
"has_more": false
}
traces[].trace_idThe trace id.
traces[].labelThe label given at creation.
traces[].statusThe trace's current status. One of open, sealing, sealed.
traces[].event_countFor a sealed trace, the number of items (events, documents and attestations) hashed at seal time; for an open trace, the live number of events excluding any trace.sealed event.
traces[].document_countLive count of anchored documents attached to the trace.
traces[].attestation_countLive count of AI attestations attached to the trace.
traces[].created_atWhen the trace was created.
traces[].sealed_atWhen the seal completed; null until the trace is sealed.
traces[].sourceHow the trace was created. One of api, dashboard.
pageThe page returned.
limitThe page size applied after clamping.
totalTotal traces matching the filter.
has_moreTrue when page * limit is below total.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key has neither the read nor the write scope.
invalid_statusstatus is set to anything other than open, sealing or sealed.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/traces/{trace_id}Get a trace
Returns one trace with its status, composite hash once sealed, live item counts, a page of its events and all of its documents and attestations.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
event_page1-based page of the events list; values below 1 are treated as 1.
event_limitEvents per page; values outside the range are clamped.
- Only events are paginated; documents and attestations are returned in full, ordered by anchored_at and created_at ascending.
- Once the trace is sealed, the events list and event_total include the trace.sealed seal event (its id is seal_event_id); the list is ordered by ingested_at ascending, so the seal event comes last.
- Poll this endpoint after POST /v1/traces/{trace_id}/seal: status moves from sealing to sealed and composite_hash, sealed_at and seal_event_id are filled in together.
- The SDK typed models for this response declare trace_id, label, status, metadata, created_at, sealed_at, composite_hash, seal_event_id, event_count and the events page, but not documents, attestations, item_counts, source or events[].source; the JSON carries them, and Ruby and PHP return the raw hash or array.
- An event_page or event_limit value that is not a whole number is rejected by the framework with a plain-text 400 before the handler runs.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const trace = await client.traces.get("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4", {
event_page: 1,
event_limit: 50,
});
console.log(trace.status, trace.composite_hash);
for (const event of trace.events) {
console.log(event.event_id, event.event_type, event.payload_hash);
}
{
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"label": "Invoice batch 2026-09",
"status": "sealed",
"metadata": {
"batch_id": "b_4471",
"region": "eu-west"
},
"created_at": "2026-09-22T08:14:07Z",
"sealed_at": "2026-09-22T09:01:54Z",
"composite_hash": "e01187fee8acb0969976df6f4cd49164e8443195165355990daa878cf7e12b41",
"event_count": 4,
"seal_event_id": "c4d7e8f1-2a3b-4c5d-9e6f-0a1b2c3d4e5f",
"source": "api",
"item_counts": {
"events": 3,
"documents": 1,
"attestations": 1
},
"events": [
{
"event_id": "3f9a2c71-6b0d-4e85-9c12-7d4e8a1b5f60",
"event_type": "invoice.received",
"payload_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"ingested_at": "2026-09-22T08:15:31Z",
"source": "api"
},
{
"event_id": "9b2d4e6f-1a3c-4d5e-8f70-1c2b3a4d5e6f",
"event_type": "invoice.approved",
"payload_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"ingested_at": "2026-09-22T08:22:45Z",
"source": "api"
},
{
"event_id": "c4d7e8f1-2a3b-4c5d-9e6f-0a1b2c3d4e5f",
"event_type": "trace.sealed",
"payload_hash": "3b428d41452a757b5466edc0520d0f3028c0335cfab5c7b6dac47531dbfcf321",
"ingested_at": "2026-09-22T09:01:54Z",
"source": "api"
}
],
"documents": [
{
"event_id": "5e8f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
"document_ref": "INV-2026-0441.pdf",
"document_hash": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"event_type": "document.anchored",
"anchored_at": "2026-09-22T08:19:02Z"
}
],
"attestations": [
{
"attestation_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"attestation_type": "output",
"model_provider": "openai",
"model_name": "gpt-4.1",
"payload_hash": "bef6d7205ca5b97289bdb4cdb60a6de5b1fd8a2e1e5a9b663f1cdea305b644ab",
"created_at": "2026-09-22T08:26:10Z"
}
],
"event_page": 1,
"event_limit": 50,
"event_total": 3,
"event_has_more": false
}
trace_idThe trace id.
labelThe label given at creation.
statusThe trace's current status. One of open, sealing, sealed.
metadataThe metadata given at creation, or null.
created_atWhen the trace was created.
sealed_atWhen the seal completed; null until the trace is sealed.
composite_hashSHA-256 over the concatenated raw item hashes in timestamp order, 64 hex characters; null until the trace is sealed.
event_countFor a sealed trace, the number of items hashed at seal time; for an open trace, event_total plus the number of documents and attestations.
seal_event_idId of the trace.sealed event written at seal time; null until sealed.
sourceHow the trace was created. One of api, dashboard.
item_counts.eventsLive count of events with this trace_id, including the trace.sealed event once sealed.
item_counts.documentsLive count of anchored documents with this trace_id.
item_counts.attestationsLive count of AI attestations with this trace_id.
events[].event_idThe event id.
events[].event_typeThe event type given at ingest, or trace.sealed for the seal event.
events[].payload_hashSHA-256 of the event payload, 64 hex characters.
events[].ingested_atWhen the event was ingested; the events list is ordered by this ascending.
events[].sourceHow the event was created. One of api, dashboard.
documents[].event_idThe document anchor's event id.
documents[].document_refThe reference given when the document was anchored.
documents[].document_hashSHA-256 of the document, 64 hex characters.
documents[].event_typeThe event type given when the document was anchored.
documents[].anchored_atWhen the document was anchored; ordered ascending.
attestations[].attestation_idThe attestation id.
attestations[].attestation_typeThe attestation type given at ingest.
attestations[].model_providerThe model provider given at ingest, or null.
attestations[].model_nameThe model name given at ingest.
attestations[].payload_hashSHA-256 of the attestation's canonical request bytes, 64 hex characters; the same value as its attestation_hash.
attestations[].created_atWhen the attestation was created; ordered ascending.
event_pageThe event page returned.
event_limitThe event page size applied after clamping.
event_totalTotal events with this trace_id, including the trace.sealed event once sealed.
event_has_moreTrue when more event pages follow.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key has neither the read nor the write scope.
trace_not_foundNo trace with this id exists in the caller's tenant.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/traces/{trace_id}Delete an empty trace
Deletes an open trace that has no events and returns its id with deleted set to true.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
- A trace is deletable only while its status is open and no event references it; sealed traces cannot be deleted at all.
- The emptiness check counts events only; anchored documents and attestations attached to the trace are not counted, so an open trace holding only documents or attestations can still be deleted.
- The row is removed from the traces table; ledger rows are never deleted.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const result = await client.traces.delete("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
console.log(result.trace_id, result.deleted);
{
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"deleted": true
}
trace_idThe id of the deleted trace.
deletedAlways true on success.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key does not have the write scope.
trace_not_foundNo trace with this id exists in the caller's tenant.
trace_not_openThe trace is sealed or currently sealing.
trace_has_eventsAt least one event carries this trace_id; seal the trace instead.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/traces/{trace_id}/sealSeal a trace
Moves an open trace to sealing and queues the seal job, which computes the composite hash, writes a signed trace.sealed event and marks the trace sealed.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
- Status transitions: open to sealing in this request (a single conditional update, so two concurrent seal calls cannot both succeed), then sealing to sealed by the worker; if the worker gives up or the job cannot be queued, the status is put back to open and the call can be repeated.
- The request needs at least one event on the trace; documents and attestations alone do not satisfy the check even though the worker includes them in the hash.
- What is sealed: the worker locks the trace row, reads payload_hash of every event, document_hash of every document and payload_hash of every attestation carrying this trace_id, and orders the three sets together by ingested_at, anchored_at and created_at ascending.
- Composite hash rule: SHA-256 over the raw 32-byte hashes concatenated in that order, so composite_hash = SHA-256(h1 || h2 || ... || hN) with hashes as bytes, not hex text.
- The worker then inserts a trace.sealed event into the ledger with payload {trace_id, composite_hash, event_count, sealed_at} (sealed_at written as RFC 3339 with a +00:00 offset), signs the canonical record {v: 1, event_id, trace_id, tenant_id, event_type, composite_hash, event_count, sealed_at} with the tenant's Ed25519 key, and stores it with retention_policy indefinite and source api (dashboard when the seal was requested without an API key).
- In the same transaction the trace gets status sealed, sealed_at, seal_event_id, composite_hash, event_count (the number of hashed items across all three types) and item_counts.
- While the status is sealing or sealed, POST /v1/events, POST /v1/document/anchor and POST /v1/ai/attestations reject the trace_id with 409.
- The worker retries a failed seal up to 6 times with exponential backoff (500 ms base, 30 s cap), except when the trace is missing or no longer in sealing status, which is not retried; once the attempts are exhausted the status is put back to open.
- The request body is ignored; the SDKs send an empty JSON object.
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const traceId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";
const seal = await client.traces.seal(traceId);
console.log(seal.status); // "sealing"
// The seal runs in the background. Poll until the status changes.
let trace = await client.traces.get(traceId);
while (trace.status === "sealing") {
await new Promise((resolve) => setTimeout(resolve, 1000));
trace = await client.traces.get(traceId);
}
console.log(trace.status, trace.composite_hash);
{
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"status": "sealing",
"message": "Seal initiated. Poll GET /v1/traces/{trace_id} for status."
}
trace_idThe trace being sealed.
statusAlways sealing when the job was queued. One of sealing.
messageFixed text telling the caller to poll GET /v1/traces/{trace_id}.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key does not have the write scope.
trace_not_foundNo trace with this id exists in the caller's tenant.
trace_not_openThe trace is already sealed or is currently sealing.
trace_emptyNo event carries this trace_id; the status is put back to open.
encode_failedThe seal job could not be serialized; the status is left as sealing, unlike the other failure paths.
seal_publish_failedThe seal job could not be queued; the status is put back to open.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/traces/{trace_id}/proofGet the proof bundle
Returns a sealed trace as a JSON bundle with every event, document and attestation, their hashes, signatures and public keys, and the seal event.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
- Only sealed traces have a bundle; an open or sealing trace answers 409 trace_not_sealed.
- events excludes the seal event, which is returned separately as seal_event; event_count is the length of events and item_count adds documents and attestations.
- To verify independently, sort every entry of events (content_hash), documents (document_hash) and attestations (payload_hash) by timestamp, concatenate the hashes as raw bytes and SHA-256 the result; it must equal composite_hash.
- For attestations, hash payload_raw (the exact canonical request bytes as a string) to reproduce payload_hash; payload is a parsed copy and re-serializing it can change the bytes.
- verification.composite_hash_valid and verification.all_signatures_valid are written as true by the handler for every bundle it returns; they are not the result of a per-request check.
- The SDK bundle models declare events, seal_event and verification only; documents, attestations, item_count and source are present in the JSON but not in the typed models. The Node sample casts the object to read them; the Python models are declared with extra="allow", so model_dump() returns them.
- Event and seal signatures cover a canonical record, not the payload shown here: for an event it is {v, event_id, tenant_id, event_type, payload_hash, request_hash, ingested_at}, for the seal event the record listed under POST /v1/traces/{trace_id}/seal; the exact signed bytes are returned hex-encoded as signed_payload by GET /v1/proof/event/{event_id} for any ledger event, including seal_event.event_id.
import { createHash } from "node:crypto";
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const bundle = await client.traces.proof("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
// The SDK's TraceProofBundle type declares events and seal_event only; the
// JSON also carries documents and attestations, so read them off the raw object.
const raw = bundle as unknown as {
events: { timestamp: string; content_hash: string }[];
documents: { timestamp: string; document_hash: string }[];
attestations: { timestamp: string; payload_hash: string }[];
};
// Recompute the composite hash: SHA-256 over the raw item hashes in
// timestamp order across events, documents and attestations.
const items = [
...raw.events.map((e) => ({ at: e.timestamp, hash: e.content_hash })),
...raw.documents.map((d) => ({ at: d.timestamp, hash: d.document_hash })),
...raw.attestations.map((a) => ({ at: a.timestamp, hash: a.payload_hash })),
].sort((a, b) => a.at.localeCompare(b.at));
const hasher = createHash("sha256");
for (const item of items) hasher.update(Buffer.from(item.hash, "hex"));
const recomputed = hasher.digest("hex");
console.log(bundle.composite_hash);
console.log(recomputed === bundle.composite_hash ? "composite hash matches" : "mismatch");
{
"version": "1.0",
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"label": "Invoice batch 2026-09",
"tenant_domain": "acme.example",
"status": "sealed",
"source": "api",
"created_at": "2026-09-22T08:14:07Z",
"sealed_at": "2026-09-22T09:01:54Z",
"composite_hash": "e01187fee8acb0969976df6f4cd49164e8443195165355990daa878cf7e12b41",
"event_count": 2,
"item_count": 4,
"events": [
{
"event_id": "3f9a2c71-6b0d-4e85-9c12-7d4e8a1b5f60",
"event_type": "invoice.received",
"payload": {
"invoice_number": "INV-2026-0441",
"amount_cents": 128500,
"currency": "EUR"
},
"content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"timestamp": "2026-09-22T08:15:31Z",
"signature": "b7c1d4e9f2a5068397c0d3e6f1a4b58d2e4f6a8c0b1d3f5e7a9c1b3d5f7e9a0c4d6e8f0a2b4c6d8e1f3a5b7c9d0e2f4a6b8c0d2e4f6a8b1c3d5e7f9a0b2c4d6e",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"
},
{
"event_id": "9b2d4e6f-1a3c-4d5e-8f70-1c2b3a4d5e6f",
"event_type": "invoice.approved",
"payload": {
"invoice_number": "INV-2026-0441",
"approved_by": "finance_lead"
},
"content_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"timestamp": "2026-09-22T08:22:45Z",
"signature": "3e5f7a9c1b2d4f6e8a0c2e4f6a8b0d1f5c7e9a1b3d5f7a9c0e2f4a6b8d0f2a4c7e9b1d3f5a7c9e0b2d4f6a8c1e3f5a7b9d0f2a4c6e8b1d3f5a7c9e0b2d4f6a8c",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"
}
],
"documents": [
{
"document_id": "5e8f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
"document_ref": "INV-2026-0441.pdf",
"document_hash": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"event_type": "document.anchored",
"signature": "9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"timestamp": "2026-09-22T08:19:02Z"
}
],
"attestations": [
{
"attestation_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"attestation_type": "output",
"model_name": "gpt-4.1",
"model_provider": "openai",
"payload": {
"type": "output",
"payload": {
"input": "Summarize invoice INV-2026-0441",
"output": "Invoice for 1,285.00 EUR, due 2026-10-22."
},
"context": {
"model_provider": "openai",
"model_name": "gpt-4.1",
"model_version": "2026-04-14"
},
"subject": null,
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4"
},
"payload_raw": "{\"type\":\"output\",\"payload\":{\"input\":\"Summarize invoice INV-2026-0441\",\"output\":\"Invoice for 1,285.00 EUR, due 2026-10-22.\"},\"context\":{\"model_provider\":\"openai\",\"model_name\":\"gpt-4.1\",\"model_version\":\"2026-04-14\"},\"subject\":null,\"trace_id\":\"7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4\"}",
"payload_hash": "bef6d7205ca5b97289bdb4cdb60a6de5b1fd8a2e1e5a9b663f1cdea305b644ab",
"signature": "5c7e9a1b3d5f7a9c0e2f4a6b8d0f2a4c7e9b1d3f5a7c9e0b2d4f6a8c1e3f5a7b9d0f2a4c6e8b1d3f5a7c9e0b2d4f6a8c3e5f7a9c1b2d4f6e8a0c2e4f6a8b0d1f",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"timestamp": "2026-09-22T08:26:10Z"
}
],
"seal_event": {
"event_id": "c4d7e8f1-2a3b-4c5d-9e6f-0a1b2c3d4e5f",
"event_type": "trace.sealed",
"content_hash": "3b428d41452a757b5466edc0520d0f3028c0335cfab5c7b6dac47531dbfcf321",
"timestamp": "2026-09-22T09:01:54Z",
"signature": "e1f3a5b7c9d0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"
},
"verification": {
"composite_hash_valid": true,
"all_signatures_valid": true
}
}
versionBundle format version, currently 1.0.
trace_idThe trace id.
labelThe label given at creation.
tenant_domainThe organization's primary domain, or null when none is set.
statusAlways sealed; other statuses are rejected with 409. One of sealed.
sourceHow the trace was created. One of api, dashboard.
created_atWhen the trace was created.
sealed_atWhen the seal completed.
composite_hashSHA-256 over the concatenated raw item hashes in timestamp order, 64 hex characters.
event_countNumber of entries in events; the seal event is not counted.
item_countNumber of entries in events, documents and attestations combined.
events[].event_idThe event id.
events[].event_typeThe event type given at ingest.
events[].payloadThe stored event payload.
events[].content_hashSHA-256 of the event payload, 64 hex characters; this is the value that enters the composite hash.
events[].timestampWhen the event was ingested; ordered ascending.
events[].signatureEd25519 signature stored with the event, 128 hex characters; empty when the row has none.
events[].public_keyEd25519 public key that signed the event, 64 hex characters.
documents[].document_idThe document anchor's event id.
documents[].document_refThe reference given when the document was anchored.
documents[].document_hashSHA-256 of the document, 64 hex characters; this is the value that enters the composite hash.
documents[].event_typeThe event type given when the document was anchored.
documents[].signatureEd25519 signature stored with the anchor, 128 hex characters.
documents[].public_keyEd25519 public key that signed the anchor, 64 hex characters.
documents[].timestampWhen the document was anchored; ordered ascending.
attestations[].attestation_idThe attestation id.
attestations[].attestation_typeThe attestation type given at ingest.
attestations[].model_nameThe model name given at ingest.
attestations[].model_providerThe model provider given at ingest, or null.
attestations[].payloadpayload_raw parsed into an object, for reading; omitted when the stored bytes could not be fetched.
attestations[].payload_rawThe exact canonical request bytes stored at ingest, as a JSON string; SHA-256 of this string reproduces payload_hash; omitted when unavailable.
attestations[].payload_hashSHA-256 of the attestation's canonical request bytes, 64 hex characters, the same value as its attestation_hash; this is the value that enters the composite hash.
attestations[].signatureEd25519 signature stored with the attestation, 128 hex characters.
attestations[].public_keyEd25519 public key that signed the attestation, 64 hex characters.
attestations[].timestampWhen the attestation was created; ordered ascending.
seal_event.event_idId of the trace.sealed event.
seal_event.event_typeAlways trace.sealed. One of trace.sealed.
seal_event.content_hashSHA-256 of the seal payload as compact JSON with the keys trace_id, composite_hash, event_count and sealed_at in that order, 64 hex characters.
seal_event.timestampWhen the seal event was written; equals sealed_at.
seal_event.signatureEd25519 signature over the canonical seal record, 128 hex characters.
seal_event.public_keyEd25519 public key that signed the seal, 64 hex characters.
verification.composite_hash_validSet to true by the handler when it returns a bundle; it is not recomputed per request.
verification.all_signatures_validSet to true by the handler when it returns a bundle; it is not recomputed per request.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key has neither the read nor the write scope.
trace_not_foundNo trace with this id exists in the caller's tenant.
trace_not_sealedThe trace's status is open or sealing.
seal_event_missingThe trace is sealed but the row for its seal_event_id could not be read.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/traces/{trace_id}/proof/pdfDownload the proof bundle as PDF
Renders the sealed trace's proof bundle as a PDF and returns it as a file attachment.
AuthorizationBearer followed by the API key (invoance_live_...); the same key in an X-API-Key header is accepted instead.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
- The PDF lists the events in ingested_at order with the seal event set apart, then the documents and the attestations; attestation payloads are printed when the stored bytes can be fetched.
- The same renderer produces the dashboard export, so the two files match.
- Error responses on this endpoint are JSON with the usual {error, message} shape even though the success body is a PDF.
import { writeFileSync } from "node:fs";
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment.
const client = new InvoanceClient();
const pdf = await client.traces.proofPdf("7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4");
writeFileSync("trace-proof.pdf", Buffer.from(pdf));
console.log("wrote trace-proof.pdf", pdf.byteLength, "bytes");
"<binary PDF body>"
(body)Raw PDF bytes; the response sets Content-Type application/pdf, Content-Disposition attachment with filename trace-{trace_id}-proof.pdf, and Cache-Control no-store.
missing_api_keyNeither an Authorization header nor an X-API-Key header is present.
invalid_authorization_schemeThe Authorization header is present but does not use the Bearer scheme.
invalid_api_key_formatThe key does not start with invoance_live_.
invalid_api_keyNo API key with this value exists.
api_key_revokedThe key exists but has been revoked.
ip_not_allowedThe key has an IP allowlist and the caller's address is not on it.
api_key_lookup_failedThe key could not be looked up in the database.
insufficient_scopeThe key has neither the read nor the write scope.
trace_not_foundNo trace with this id exists in the caller's tenant.
trace_not_sealedThe trace's status is open or sealing.
pdf_generation_failedThe renderer could not produce the document.
db_errorA database query failed.
rate_limitedThe tenant exceeded its per-second (plan rate_limit_per_sec, default 10) or per-minute (60 times that) budget; the body carries retry_after, plan_tier and limit_per_sec and the response sets Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining.
/v1/proof/trace/{trace_id}Get the public proof
Returns a sealed trace that its owner has made public, with every item's hash, signature and public key, without an API key.
trace_idThe trace id returned by POST /v1/traces; a value that is not a UUID is rejected by the framework path extractor with a plain-text 400.
- This is the public counterpart of GET /v1/traces/{trace_id}/proof: same items and hashes, no API key, and only for traces whose owner switched on public verification in the dashboard (the traces.public flag, settable on sealed traces only).
- Attestation payload bytes are included only when the owner also enabled ai_public; otherwise each attestation carries its payload_hash, signature and public key but no payload.
- The seal event is returned in seal_event with the seal payload {trace_id, composite_hash, event_count, sealed_at} as its payload; events holds the other events in ingested_at order.
- Timestamps on this endpoint are RFC 3339 strings with microseconds and a +00:00 offset, not the Z form used by the API key endpoints.
- Each successful call records a view in trace_proof_view_events with source public_url, the caller's IP address, user agent and country.
- A limiter configured for 2 requests per second and 10 per minute is layered on the proof routes, but it reads its per-caller key from a request extension that no middleware sets, so it passes every request through and this endpoint does not answer 429.
- Field names differ from the API key bundle: documents[].event_id instead of document_id, and ingested_at, anchored_at and created_at instead of timestamp.
- No SDK wraps this endpoint; call it with any HTTP client.
// No API key: the public proof endpoint is unauthenticated.
const traceId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";
const res = await fetch("https://api.invoance.com/v1/proof/trace/" + traceId);
if (!res.ok) {
throw new Error(res.status + " " + (await res.text()));
}
const proof = await res.json();
console.log(proof.issuer_name, proof.composite_hash);
console.log(proof.events.length, "events", proof.documents.length, "documents", proof.attestations.length, "attestations");
{
"version": "1.0",
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"label": "Invoice batch 2026-09",
"issuer_name": "Acme Finance GmbH",
"tenant_domain": "acme.example",
"domain_verified": true,
"status": "sealed",
"created_at": "2026-09-22T08:14:07.482913+00:00",
"sealed_at": "2026-09-22T09:01:54.620015+00:00",
"composite_hash": "e01187fee8acb0969976df6f4cd49164e8443195165355990daa878cf7e12b41",
"event_count": 2,
"item_counts": {
"events": 2,
"documents": 1,
"attestations": 1
},
"events": [
{
"event_id": "3f9a2c71-6b0d-4e85-9c12-7d4e8a1b5f60",
"event_type": "invoice.received",
"payload": {
"invoice_number": "INV-2026-0441",
"amount_cents": 128500,
"currency": "EUR"
},
"content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"signature": "b7c1d4e9f2a5068397c0d3e6f1a4b58d2e4f6a8c0b1d3f5e7a9c1b3d5f7e9a0c4d6e8f0a2b4c6d8e1f3a5b7c9d0e2f4a6b8c0d2e4f6a8b1c3d5e7f9a0b2c4d6e",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"ingested_at": "2026-09-22T08:15:31.006240+00:00"
},
{
"event_id": "9b2d4e6f-1a3c-4d5e-8f70-1c2b3a4d5e6f",
"event_type": "invoice.approved",
"payload": {
"invoice_number": "INV-2026-0441",
"approved_by": "finance_lead"
},
"content_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"signature": "3e5f7a9c1b2d4f6e8a0c2e4f6a8b0d1f5c7e9a1b3d5f7a9c0e2f4a6b8d0f2a4c7e9b1d3f5a7c9e0b2d4f6a8c1e3f5a7b9d0f2a4c6e8b1d3f5a7c9e0b2d4f6a8c",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"ingested_at": "2026-09-22T08:22:45.310556+00:00"
}
],
"documents": [
{
"event_id": "5e8f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
"document_ref": "INV-2026-0441.pdf",
"document_hash": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"event_type": "document.anchored",
"signature": "9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"anchored_at": "2026-09-22T08:19:02.751108+00:00"
}
],
"attestations": [
{
"attestation_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"attestation_type": "output",
"model_provider": "openai",
"model_name": "gpt-4.1",
"payload_hash": "bef6d7205ca5b97289bdb4cdb60a6de5b1fd8a2e1e5a9b663f1cdea305b644ab",
"signature": "5c7e9a1b3d5f7a9c0e2f4a6b8d0f2a4c7e9b1d3f5a7c9e0b2d4f6a8c1e3f5a7b9d0f2a4c6e8b1d3f5a7c9e0b2d4f6a8c3e5f7a9c1b2d4f6e8a0c2e4f6a8b0d1f",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"created_at": "2026-09-22T08:26:10.094377+00:00"
}
],
"seal_event": {
"event_id": "c4d7e8f1-2a3b-4c5d-9e6f-0a1b2c3d4e5f",
"event_type": "trace.sealed",
"payload": {
"trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
"composite_hash": "e01187fee8acb0969976df6f4cd49164e8443195165355990daa878cf7e12b41",
"event_count": 4,
"sealed_at": "2026-09-22T09:01:54.620015+00:00"
},
"content_hash": "3b428d41452a757b5466edc0520d0f3028c0335cfab5c7b6dac47531dbfcf321",
"signature": "e1f3a5b7c9d0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8f1a3b5c7",
"public_key": "1f8e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
"ingested_at": "2026-09-22T09:01:54.620015+00:00"
}
}
versionBundle format version, currently 1.0.
trace_idThe trace id.
labelThe label given at creation.
issuer_nameThe issuer name snapshotted on the most recent event in the trace, falling back to the organization's issuer name; null when neither is set.
tenant_domainThe organization's primary domain, or null.
domain_verifiedWhether the organization has verified that domain; null when unknown.
statusAlways sealed; other statuses are rejected with 409. One of sealed.
created_atWhen the trace was created, RFC 3339 with a +00:00 offset.
sealed_atWhen the seal completed, RFC 3339 with a +00:00 offset.
composite_hashSHA-256 over the concatenated raw item hashes in timestamp order, 64 hex characters.
event_countNumber of entries in events; the seal event is not counted.
item_countsCounts stored at seal time as {events, documents, attestations}; null for traces sealed before the counts were recorded.
events[].event_idThe event id.
events[].event_typeThe event type given at ingest.
events[].payloadThe stored event payload.
events[].content_hashSHA-256 of the event payload, 64 hex characters.
events[].signatureEd25519 signature stored with the event, 128 hex characters, or null.
events[].public_keyEd25519 public key that signed the event, 64 hex characters, or null.
events[].ingested_atWhen the event was ingested, RFC 3339 with a +00:00 offset; ordered ascending.
documents[].event_idThe document anchor's event id.
documents[].document_refThe reference given when the document was anchored.
documents[].document_hashSHA-256 of the document, 64 hex characters.
documents[].event_typeThe event type given when the document was anchored.
documents[].signatureEd25519 signature stored with the anchor, 128 hex characters, or null.
documents[].public_keyEd25519 public key that signed the anchor, 64 hex characters, or null.
documents[].anchored_atWhen the document was anchored, RFC 3339 with a +00:00 offset.
attestations[].attestation_idThe attestation id.
attestations[].attestation_typeThe attestation type given at ingest.
attestations[].model_providerThe model provider given at ingest, or null.
attestations[].model_nameThe model name given at ingest.
attestations[].payload_hashSHA-256 of the attestation's canonical request bytes, 64 hex characters; the same value as its attestation_hash.
attestations[].payloadThe exact canonical request bytes stored at ingest, as a JSON string whose SHA-256 is payload_hash; present only when the owner enabled ai_public for the trace, omitted otherwise.
attestations[].signatureEd25519 signature stored with the attestation, 128 hex characters, or null.
attestations[].public_keyEd25519 public key that signed the attestation, 64 hex characters, or null.
attestations[].created_atWhen the attestation was created, RFC 3339 with a +00:00 offset.
seal_eventThe trace.sealed event in the same shape as an events[] entry, with the seal payload {trace_id, composite_hash, event_count, sealed_at} as its payload; null if the seal event row is missing.
trace_not_foundNo trace with this id exists in any tenant.
trace_not_sealedThe trace exists but its status is open or sealing.
trace_not_publicThe trace is sealed but its owner has not enabled public verification.
db_errorA database query failed.
| open | The status a trace starts in; items can be attached and the trace can be deleted while it has no events. |
|---|---|
| sealing | The status set by POST /v1/traces/{trace_id}/seal while the worker computes the composite hash; new items are rejected and the status returns to open if the job fails. |
| sealed | The final status; the trace has a composite hash, a signed seal event, and accepts no changes. |
| item | An event, an anchored document or an AI attestation that carries the trace's id. |
| composite hash | SHA-256 over the raw 32-byte hashes of every item in the trace, concatenated in timestamp order across events, documents and attestations. |
| seal event | A trace.sealed event written to the ledger at seal time, signed with the tenant's Ed25519 key, carrying the composite hash, item count and seal time. |
| proof bundle | The JSON from GET /v1/traces/{trace_id}/proof: every item with its hash, signature and public key, plus the seal event. |
| public proof | The same data served at GET /v1/proof/trace/{trace_id} without an API key, for traces whose owner enabled public verification. |