> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Decision Receipts

> The cryptography behind the audit trail — Ed25519 receipts, Merkle batching, HSM-signed roots, RFC 3161 timestamps — and how to verify every layer, online or offline.

Every [record envelope](/record/introduction) — action, retrieval, and human-adjudication decisions alike — is attested by a **decision receipt**: an Ed25519 signature over the decision's canonical payload, stored separately from the envelope it proves. Receipts are then Merkle-batched under an HSM-signed root, timestamped by an independent authority, and chained into an append-only transparency log.

This page specifies the exact cryptography so you — or your auditor — can verify it independently.

<Note>
  Receipt issuance is plan-dependent. Decisions on every plan are still recorded in the queryable audit logs.
</Note>

***

## What a receipt contains

| Field               | Encoding       | Meaning                                                        |
| ------------------- | -------------- | -------------------------------------------------------------- |
| `signature`         | base64url      | Ed25519 signature over the canonical payload bytes             |
| `public_key`        | base64url      | The signing public key, as SPKI DER (44 bytes decoded)         |
| `payload_hash`      | hex (64 chars) | SHA-256 of the canonical payload bytes — the Merkle leaf input |
| `canonical_payload` | JSON           | The exact alphabetically-key-sorted payload that was signed    |

Once the receipt is committed to a batch it also carries its Merkle coordinates: the batch it belongs to, its leaf index, and its leaf hash. The dashboard's record verification page surfaces all of these fields for any record.

## When receipts are issued

Signing is **asynchronous** — scheduled immediately after the envelope persists, entirely off the decision hot path. Your agents never wait on cryptography; a decision's latency is identical with receipts on or off.

Asynchronous does not mean best-effort. Unbatched receipts *are* the durable batching queue: if a batching sweep fails, the next sweep picks up the same receipts. A record's verification result reports `attestationStatus: "timestamp_pending"` between signing and batch commitment — typically a minute or two — and `"signed"` once the full chain verifies.

***

## The exact cryptography

### Canonicalization

The signed payload is the envelope's `event` object, serialized deterministically:

1. Object keys are sorted **alphabetically, recursively** (arrays keep their order; primitives pass through unchanged).
2. The sorted structure is serialized with compact `JSON.stringify` — no whitespace.
3. The signature and hash operate on the **UTF-8 bytes** of that string.

### Hash and signature

* `payload_hash` = SHA-256 over the canonical bytes, hex-encoded.
* `signature` = Ed25519 over the **canonical payload bytes themselves** — not over the hash.

<Warning>
  The most common verification mistake is signing-recipe drift: verifying the Ed25519 signature against the SHA-256 hash, decoding fields as standard base64 instead of **base64url**, or parsing `public_key` as a raw 32-byte key instead of **SPKI DER**. Any one of these makes every genuine receipt appear forged. The sample below matches the real recipe.
</Warning>

### Key management

In production the signing key is mandatory — the signer refuses to fall back to an ephemeral key, so a receipt that exists is always verifiable against the published key. Receipts produced by the hardened strategies record which one signed them (a receipt with no recorded strategy was signed by the default strategy), and an optional hardened mode signs every receipt inside the HSM itself so the private key never enters an application process (for those receipts, the signed message is the 32-byte `payload_hash` digest rather than the canonical bytes — batch roots are signed the same way). The receipt's stored `public_key` is authoritative for verification, so key rotation never invalidates history.

***

## Verify with one call (recommended)

Ask the platform to re-derive and re-check the entire attestation chain for a record:

```bash theme={null}
curl "https://api.visiqlabs.com/record/envelopes/9f4c1a3e-8f2b-4c1d-9e5a-2b7c8d0f1a42/verify" \
  -H "Authorization: Bearer <management-token>"
```

```json theme={null}
{
  "recordId": "9f4c1a3e-8f2b-4c1d-9e5a-2b7c8d0f1a42",
  "verified": true,
  "attestationPending": false,
  "attestationStatus": "signed",
  "checks": {
    "integrity":       { "pass": true, "detail": "recomputed SHA-256 matches receipt (58a9c1d2e3f40516…)" },
    "leafSignature":   { "pass": true, "detail": "Ed25519 leaf signature valid for the canonical decision payload (env signer)" },
    "merkleInclusion": { "pass": true, "detail": "leaf #142 proven in Merkle root via 8-step recomputed proof" },
    "kmsRootSignature":{ "pass": true, "detail": "chain_hash signature confirmed LIVE by AWS KMS (HSM key active)", "liveKmsVerified": true },
    "timestamp":       { "pass": true, "detail": "RFC-3161 token verified against DigiCert chain; attested 2026-07-03T17:23:12Z", "source": "batch-root" },
    "ingestAnchor":    { "pass": true, "detail": "ingest anchor verified: event == content_hash == receipt (1f6b3c9a2d4e5f70…)" }
  },
  "evidence": {
    "source": "action",
    "decision": "deny",
    "emittedAt": "2026-07-03T17:22:41.118Z",
    "payloadHash": "58a9c1d2e3f40516…",
    "merkleRoot": "b47e2f91c3a8d605…",
    "leafIndex": 142,
    "tsaGenTime": "2026-07-03T17:23:12Z",
    "chainPosition": { "batchSeq": 3121, "prevRoot": "77d1a0be94c2f358…", "chainSigned": true }
  }
}
```

Checks fail closed: a check that cannot be completed reports `pass: false` with a reason — never a silent pass — with one deliberate exception: the ingest anchor on legacy records that predate anchor recording. `verified` is `true` only when all applicable checks pass. The Merkle inclusion proof is **recomputed on demand** from the batch's retained leaf set — it must reproduce the signed root exactly — and the root check performs a live AWS KMS `Verify` round-trip, confirming the HSM key itself vouches for the signature.

These endpoints sit on the management surface: harness keys used by your agents are deliberately confined to the operational SDK routes and receive `403` here. The dashboard's record verification page runs the identical checks — click any record pill under **Harness → Runtime Enforcement**. For scripted access, see [Platform Automation](/automation/introduction).

***

## Verify offline (TypeScript)

Receipts also verify with no VisIQ dependency at all — Node's built-in crypto is enough. Copy the `event` and receipt fields from the record's verification page in the dashboard:

```typescript theme={null}
import { createHash, createPublicKey, verify } from "node:crypto";

// The envelope's event object, exactly as recorded.
const event = {
  target_app: "stripe",
  action: "issue_refund",
  context: { amount: 500, currency: "USD" },
};

// The receipt fields, as shown on the record's verification page.
const receipt = {
  signature: "<base64url Ed25519 signature>",
  public_key: "<base64url SPKI DER public key>",
  payload_hash: "<64-char hex SHA-256>",
};

// 1. Canonicalize: sort object keys alphabetically, recursively.
//    Arrays keep their order; primitives pass through.
function canonicalize(value: unknown): unknown {
  if (value === null || typeof value !== "object") return value;
  if (Array.isArray(value)) return value.map(canonicalize);
  const sorted: Record<string, unknown> = {};
  for (const key of Object.keys(value).sort()) {
    sorted[key] = canonicalize((value as Record<string, unknown>)[key]);
  }
  return sorted;
}
const canonicalBytes = Buffer.from(JSON.stringify(canonicalize(event)), "utf8");

// 2. The payload hash is SHA-256 over the canonical bytes.
const payloadHash = createHash("sha256").update(canonicalBytes).digest("hex");
if (payloadHash !== receipt.payload_hash) {
  throw new Error("payload hash mismatch — the event was altered");
}

// 3. The Ed25519 signature covers the canonical bytes themselves.
//    Ed25519 uses one-shot verify with algorithm null (no digest step).
const publicKey = createPublicKey({
  key: Buffer.from(receipt.public_key, "base64url"),
  format: "der",
  type: "spki",
});
const ok = verify(
  null,
  canonicalBytes,
  publicKey,
  Buffer.from(receipt.signature, "base64url"),
);
console.log(ok ? "receipt verified" : "SIGNATURE INVALID — record tampered or forged");
```

<Note>
  This verifies the default signing strategy. A receipt whose recorded signing strategy is the HSM-per-receipt mode signs the 32-byte `payload_hash` digest instead — substitute `Buffer.from(receipt.payload_hash, "hex")` as the message in step 3. The one-call endpoint above handles both automatically.
</Note>

***

## Merkle inclusion and the checkpoint chain

Receipts are batched into an RFC 6962-style Merkle tree with domain-separated hashing:

* **Leaf hash** = `SHA-256( 0x00 ‖ payload_hash_bytes )`
* **Node hash** = `SHA-256( 0x01 ‖ left ‖ right )`
* An unpaired trailing node is promoted unchanged to the next level — never hashed with a copy of itself.

The batch root is signed **once** with the HSM-backed key, and one RFC 3161 timestamp covers the root — so a single signature and a single timestamp attest every receipt in the batch via its inclusion proof. That is what makes 100% signing-and-timestamping coverage hold at any decision volume.

Batches then form an append-only **hash chain**: each batch carries a monotonic sequence number, a commitment to the previous batch's root, and a running accumulator

```
chain_hash[k] = SHA-256( 0x02 ‖ chain_hash[k-1] ‖ root_hash[k] )
```

with the batch signature covering `chain_hash` — so one trusted tip signature vouches for the entire history. Deleting a batch leaves a gap, reordering breaks the accumulator, and editing any root breaks the recomputation. Two management-API endpoints expose this to any authenticated caller (the feed itself is tenant-neutral — it contains only roots, signatures, and counts):

* `GET /record/checkpoints` — the transparency-log checkpoint feed: signed roots, signatures, leaf counts, and timestamp times, newest first. Also browsable in the dashboard under **Settings → Checkpoint feed** (organization scope).
* `GET /record/chain/consistency` — walks a chain segment and proves, purely by recomputation, that no batch was deleted, reordered, or tampered with.

Checkpoints are additionally published to write-once (WORM) object storage under compliance-mode object locking — an externally held copy that neither VisIQ nor its cloud provider can alter or delete before retention expires. A silent rewrite of history is therefore detectable *even by us*: the audit trail does not ask you to trust the party that operates it.

***

## RFC 3161 trusted timestamps

Each batch root is countersigned by **DigiCert's timestamp authority**. The DER-encoded timestamp token binds the root hash to a time asserted by DigiCert — an independent third party — and verification checks the token against DigiCert's certificate chain. Combined with the chain above, this proves a decision existed *no later than* the attested time, on evidence that does not depend on VisIQ's clocks or word.

For long-horizon audits, batches can also carry frozen revocation evidence for the timestamp certificate chain, so tokens remain independently verifiable years later, after certificates expire or responders disappear.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Audit Trail" icon="file-signature" href="/record/introduction">
    What gets recorded, the envelope schema, and how to consume the trail.
  </Card>

  <Card title="Platform Automation" icon="robot" href="/automation/introduction">
    Authenticate scripts and CI against the management API.
  </Card>
</CardGroup>
