Home
Home/Developers/Traces/sealing
Ed25519 signaturesSHA-256 hashes8 SDKs
Status
Sign inStart free
Home
Start here
Overview
Authentication
Errors
FAQ
API
Events
Canonical JSON and hashes
Documents
Anchoring a file
AI attestations
Attestation schemaVerifying attestations
Traces
Sealing a trace
Audit logs
OrganizationsStreamsPortalPublic proofEvent schemaExporting eventsIntegrationsEmbeddable viewer
All endpoints
Reference
SDKs
Verification
Docs · Traces

Sealing a trace

A trace groups events, anchored documents and AI attestations into one bundle. This page covers its statuses, how items join, the composite hash and the proof.

Traces APISeal endpoint
sha-256 · 3a352297…2592
sha-256 · 589ed6d0…2bf7

A trace groups the events, anchored documents and AI attestations of one process, such as an invoice run. You create it, attach items by passing its trace_id, and seal it when the process is done.

Sealing runs in the background. A worker orders every item hash by time, hashes the concatenation with SHA-256, and writes a trace.sealed event signed with the tenant key.

A sealed trace takes no more items and cannot be deleted. The proof endpoints then return every item with its hash, signature and public key, plus the seal event.

CreatePOST /v1/traces with a label; the trace starts open
Add itemstrace_id on POST /v1/events, POST /v1/document/anchor and POST /v1/ai/attestations
SealPOST /v1/traces/{trace_id}/seal answers 202; a worker finishes the seal
Statusesopen, sealing, sealed
Composite hashSHA-256 over the raw item hashes in timestamp order
ProofGET /v1/traces/{trace_id}/proof, the public page, or the PDF
Lifecycle
  1. Create · open

    POST /v1/traces stores the trimmed label and optional metadata with status open. One open trace per label per tenant.

  2. Add items · open

    Events, documents and attestations carry trace_id in their own create call. The trace must still be open.

  3. Seal · sealing

    POST /v1/traces/{trace_id}/seal moves open to sealing in one conditional update and queues the seal job. The call answers 202.

  4. Done · sealed

    The worker hashes the items, writes a signed trace.sealed event and sets status sealed in one transaction. Poll GET /v1/traces/{trace_id}.

Status changes
open to sealingPOST /v1/traces/{trace_id}/seal, only while the status is open. A second call answers 409 trace_not_open.
sealing to sealedThe worker, with the trace row locked, in the same transaction that writes the seal event.
sealing to openThe seal call finds no event (400 trace_empty), cannot queue the job (500 seal_publish_failed), or the worker gives up.
stays sealing500 encode_failed, when the seal job cannot be serialized. The status is not put back.
sealedFinal. No item can join, the trace cannot be deleted, and the proof endpoints answer.

When a seal fails

  • The worker tries up to 6 times. The wait doubles from 500 ms, caps at 30 s, and carries jitter.
  • A trace that is missing or no longer sealing is not retried. Every other error is.
  • After the last attempt the status goes back to open and you can seal again.
  • Nothing is written on failure. The seal event and the composite hash are committed together or not at all.

Deleting a trace

  • DELETE /v1/traces/{trace_id} removes an open trace that has no events.
  • A sealed or sealing trace answers 409 trace_not_open.
  • An open trace with at least one event answers 409 trace_has_events. Seal it instead.
  • The check counts events only. Documents and attestations on the trace do not block a delete.
How items join

There is no attach endpoint. An item joins a trace by carrying trace_id in the call that creates it. Each of the three create calls runs the same check.

  • The trace is looked up by id inside the calling key's tenant. A trace owned by another tenant is not found.
  • The status must be open. Sealed and sealing are refused.
  • The check runs before the item is queued. A refused item is not stored.
  • A stored item keeps the trace_id and appears in GET /v1/traces/{trace_id} under events, documents or attestations.
Where trace_id goes
POST /v1/eventsId of an open trace owned by the same tenant; the event is attached to that trace and the trace must not be sealed or sealing. Endpoint
POST /v1/document/anchorId of an open trace owned by the tenant; the document is attached to it and the trace must not be sealed or sealing. Endpoint
POST /v1/ai/attestationsAn open trace owned by the tenant; the attestation is attached to it and included when the trace is sealed. Endpoint
Errors
Trace not in your tenant404 trace_not_found from all three endpoints.
Trace is sealed409 trace_sealed from events; 409 trace_not_open from documents and attestations.
Trace is sealing409 trace_sealing from events; 409 trace_not_open from documents and attestations.
Any other status409 trace_invalid_status from events; 400 trace_invalid_status from documents and attestations.
Composite hash

One SHA-256 over every item hash in the trace, in the order the items were recorded. Each kind of item contributes the hash it already has.

eventpayload_hash, the SHA-256 of the canonical payload. Ordered by ingested_at.
documentdocument_hash, the 64 hex characters the caller sent when anchoring. Ordered by anchored_at.
attestationpayload_hash, the SHA-256 of the canonical request bytes. Ordered by created_at.
  • Lock the trace row and confirm the status is still sealing.
  • Read every item with this trace_id from the three tables into one list, ordered by timestamp across all kinds.
  • Concatenate the raw 32-byte hashes in that order. Hex text is not used.
  • SHA-256 the concatenation. The 32-byte result is composite_hash, returned as 64 hex characters.
  • The seal event, the label and the metadata are not inputs.
Sealcomposite_hash = SHA-256(h1 ‖ h2 ‖ … ‖ hN)
h1 · eventvendor.submitted5be1…09af
h2 · documentw9_acme.pdfc27d…41e0
h3 · airisk_summarye90a…7b13
h4 · eventvendor.approved18f4…d6c2
composite_hashSHA-256(h1 ‖ h2 ‖ h3 ‖ h4)d41c7e0a…b92f5a36Sealed · signed · closed to writes
import { createHash } from "node:crypto";

const traceId = "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4";

const res = await fetch("https://api.invoance.com/v1/traces/" + traceId + "/proof", {
  headers: { Authorization: "Bearer " + process.env.INVOANCE_API_KEY },
});
if (!res.ok) throw new Error(res.status + " " + (await res.text()));
const bundle = await res.json();

// One list across all three kinds, in timestamp order.
const key = (t) => t.replace(/(\.\d+)?Z$/, (_, frac) => (frac ?? ".").padEnd(10, "0"));
const items = [
  ...bundle.events.map((e) => [e.timestamp, e.content_hash]),
  ...bundle.documents.map((d) => [d.timestamp, d.document_hash]),
  ...bundle.attestations.map((a) => [a.timestamp, a.payload_hash]),
].sort(([a], [b]) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));

// SHA-256 over the raw 32-byte hashes, concatenated in that order.
const hash = createHash("sha256");
for (const [, hex] of items) hash.update(Buffer.from(hex, "hex"));
const recomputed = hash.digest("hex");

console.log(recomputed === bundle.composite_hash ? "composite hash matches" : "mismatch");
Seal event

The worker writes one more event to the ledger, of type trace.sealed. It carries the composite hash and is signed like any event.

event_typetrace.sealed
payload{trace_id, composite_hash, event_count, sealed_at} as compact JSON in that key order. sealed_at carries a +00:00 offset.
payload as readPostgres stores the payload as JSONB and returns the keys reordered. Hash the declared order.
payload_hashSHA-256 of the compact payload. request_hash is the same value.
signed_payload{v: 1, event_id, trace_id, tenant_id, event_type, composite_hash, event_count, sealed_at}, compact JSON in that order. Here sealed_at ends in Z.
signaturePlain Ed25519 over signed_payload with the tenant key. The same key and method sign every event.
stored asA ledger event with this trace_id, retention indefinite, source api or dashboard, ingested_at equal to sealed_at.

In the same transaction the trace gets status sealed, sealed_at, seal_event_id, composite_hash, event_count and item_counts. event_count here counts every hashed item, not only events.

{
  "trace_id": "7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4",
  "composite_hash": "e01187fee8acb0969976df6f4cd49164e8443195165355990daa878cf7e12b41",
  "event_count": 4,
  "sealed_at": "2026-09-22T09:01:54.620015+00:00"
}
Proof and PDF

GET /v1/traces/{trace_id}/proof

  • Needs an API key with the read scope and a sealed trace. Open or sealing answers 409 trace_not_sealed.
  • events lists every event except the seal, in ingested_at order, with payload, content_hash, signature and public_key.
  • documents carry document_hash and attestations carry payload_hash, each with signature and public_key. Attestations add payload_raw, the exact bytes behind payload_hash, when object storage returns them.
  • seal_event holds the trace.sealed event: content_hash, signature and public_key.
  • event_count is the length of events. item_count adds documents and attestations.
  • verification.composite_hash_valid and all_signatures_valid are written as true on every bundle. They are not recomputed per request.

/proof/trace/{trace_id}

  • /proof/trace/{trace_id} reads GET /v1/proof/trace/{trace_id}, which needs no API key and answers only for sealed traces the owner made public.
  • The page recomputes the composite hash in the browser, SHA-256 over the raw item hashes in time order, and compares it with the recorded value.
  • It also checks that the listed items match item_counts recorded at sealing, and that a seal event with a signature is on record.
  • It does not verify the seal signature. The signed record includes tenant_id, which the public proof does not return.
  • A visitor can paste a composite hash or an item hash and the page compares it locally. Nothing pasted is sent anywhere.

GET /v1/traces/{trace_id}/proof/pdf

  • GET /v1/traces/{trace_id}/proof/pdf returns the bundle as a PDF attachment named trace-{trace_id}-proof.pdf.
  • Same rules as the JSON bundle: API key with the read scope, sealed traces only, 409 trace_not_sealed otherwise.
  • The renderer is the one the dashboard export uses, so the two files match.
{
  "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
  }
}
EndpointsTraces API
POST/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
Questions
Can I seal a trace that holds only documents or attestations?
No. The seal call counts events with this trace_id and answers 400 trace_empty when there are none. Documents and attestations are hashed once at least one event is present.
What happens to items sent while the trace is sealing?
They are refused: 409 trace_sealing from events, 409 trace_not_open from documents and attestations. Nothing is stored.
Can I reopen a sealed trace?
No. The status is final. Create a new trace; the label is free again once the old trace is sealed.
Why does GET /v1/traces/{trace_id} list one more event than the proof bundle?
Once sealed, the trace.sealed event carries the trace_id and shows in the get endpoint's events list. The proof bundle returns it separately as seal_event.

Proof infrastructure. Records are hashed, signed with your organization's Ed25519 key, and stored append-only, so anyone can check them later.

Products

  • Audit Logs
  • Event Ledger
  • AI Attestation
  • Document Anchoring
  • Traces

Developers

  • Documentation
  • API reference
  • SDKs
  • How it works
  • How traces seal
  • System status

Verify

  • Audit Log
  • Event
  • AI Attestation
  • Document
  • Trace

Company

  • Why Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Resources
  • Help center
  • Contact
© 2026 Invoance
PrivacyLegal noticeLegal FAQGitHubLinkedInX