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 schema
Verifying attestations
Traces
Sealing a trace
Audit logs
Organizations
Streams
Portal
Public proof
Event schema
Exporting events
Integrations
Clerk
Auth0
Embeddable viewer
All endpoints
Reference
SDKs
Node.js
Python
Go
Java
Ruby
Rust
.NET
PHP
REST
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

  • Company overview
  • What is Invoance
  • Pricing
  • Security
  • Compliance teams
  • Finance teams
  • Partners
  • Brand assets
  • Resources
  • Help center
  • Contact
© 2025 – 2026 Invoance, Inc. All rights reserved.© 2026 Invoance, Inc. All rights reserved.
PrivacyLegal noticeLegal FAQ