التوثيق منشور باللغة الإنجليزية.

DocumentationTrust

Proof bundles

The file a person downloads about one record: what is in it, the chain of checks it supports, and the legitimate reasons the record itself is absent.

What a bundle is

A bundle is a self-contained JSON file about one record — a ledger entry, an audit event or a public transparency fact. A person gets it from their own account and can hand it to anybody: a regulator, a lawyer, you. Nothing in it needs UnifyID to be online to check.

`format: "unifyid.proof-bundle"`, `version: 1`

generatedAt
When the file was produced.
subject
kind — one of ledger_entry, audit_event, transparency_fact — plus id, eventId, stream, occurredAt and a plain-language description for whoever opens the file.
state
recorded, sealed, anchored_one_channel or anchored_both. Today the reachable values are the first two; the other two become reachable when anchoring starts at launch.
recordWithheld
A sentence saying why the record itself is not in the file, or null when it is present. See below.
proof
The inclusion proof, or null while the record is not yet sealed.
anchors
{ unifyid_contract, sign_protocol }, each an anchor reference or null.
keysUrl
Where UnifyID publishes its checkpoint keys. Fetch them yourself, or use a copy you already trust.
howToVerify
A line of instructions. It names the integrity verifier — read the note at the end of this page before following it.

A set of bundles in one file is format: "unifyid.proof-bundle-set", version: 1, with generatedAt and a bundles array — used when a person exports everything about one recipient on their data-sharing ledger.

The salted leaf

The inclusion proof's leaf object is where a record becomes a hash. It carries state, leafSalt, contentHash and leafHash.

The RFC 6962 leaf, with UnifyID's salt
leafHash = SHA-256( 0x00 || leafSalt || contentHash )     // 65 input bytes

0x00         one byte, the RFC 6962 leaf prefix
leafSalt     32 random bytes, assigned by the database when the event was stored
contentHash  32 bytes: SHA-256 of the canonical JSON of the hashed envelope

The salt is not decoration. Without it, anybody who could guess a record's exact contents could hash it and test that guess against a published root or against a proof somebody else shared. With it, confirming a guess requires the salt, and the salt only travels inside a proof the record's owner chose to hand over.

Canonical JSON, for check one below: object keys sorted by code unit, no whitespace, numbers and strings exactly as JSON writes them. Legacy rows at schema version 0 have no envelope and skip that check.

The chain of checks

Ten checks lead from a record to a chain. Each one only means something because the one before it held.

Checks 9 and 10 cannot pass today: nothing of UnifyID's log is anchored until anchoring starts on Base mainnet at launch.

#CheckWhat it establishes
1recordThe record in the file — or one you supply yourself — hashes to the sealed contentHash. A single changed field fails here.
2leafThe salted leaf is SHA-256(0x00 || leafSalt || contentHash).
3inclusionThe RFC 6962 inclusion proof leads from that leaf to the stream's Merkle root.
4stream_headThat root is the head this checkpoint commits to for that stream.
5combined_rootThe five stream heads combine to the checkpoint's combinedRoot.
6statementThe 256-byte statement decodes, matches the view, and hashes to the checkpoint fingerprint.
7signing_keyThe statement names one of UnifyID's published keys.
8signatureThat key signed the statement — ES256, low-S.
9unifyid_contractUnifyID's contract on Base holds this fingerprint at this sequence, with enough confirmations. Undecided until anchoring starts at launch.
10sign_protocolSign Protocol's attestation, found through UnifyID's hook, carries the identical statement and signature. Undecided until then, for the same reason.

Checks 1 to 8 are the ones you can run today, and steps 6 to 8 are exactly what Verify a checkpoint yourself walks through. Steps 1 to 5 need the proof file, which needs the record's owner.

The inclusion proof

`format: "unifyid.audit.inclusion-proof"`, `version: 1`
{
  "format": "unifyid.audit.inclusion-proof",
  "version": 1,
  "stream": "disclosure",
  "eventId": "uuid",
  "sequence": "42",
  "leafIndex": "41",
  "leaf": {
    "state": "present",
    "leafSalt": "hex64",
    "contentHash": "hex64",
    "leafHash": "hex64"
  },
  "treeSize": "120",
  "rootHash": "hex64",
  "auditPath": ["hex64"],
  "streamHeads": [{ "stream": "audit", "treeSize": "10", "rootHash": "hex64" }],
  "checkpoint": { "…": "the checkpoint view" },
  "envelope": { "canonicalization": "unifyid-canonical-json-v1", "schemaVersion": 2, "value": {} }
}

leafIndex is sequence - 1; stream sequences start at 1 and have no gaps. auditPath is the RFC 6962 path, bottom-up, the leaf's sibling first — verify it with RFC 9162 section 2.1.3.2. streamHeads always lists all five streams in the combined-root order, which is what lets the holder recompute combinedRoot without being told the per-stream roots publicly.

envelope is null unless it was asked for. When present it is the exact value the content hash was computed over — which is what makes check one possible without trusting the file's own summary of the record.

A consistency proof, format: "unifyid.audit.consistency-proof", does the other job: given two checkpoints it shows that every stream only grew between them, with an RFC 6962 consistency proof per stream. It is empty when the two tree sizes are equal, and there is none from an empty tree.

A record can legitimately be absent

Two different absences, with two different meanings. Neither is a failure, and confusing them with tampering is the mistake this section exists to prevent.

recordWithheldthe content was not included
The proof is there; the record's contents are not. The default sentence is "The record itself is not in this file. The proof still shows it was sealed and has not changed." You can still run checks 2 to 8. Check one waits for a copy of the record, which you can supply yourself and compare.
leaf.state: "tombstone"the record was purged
Retention deleted the event. leafSalt and contentHash are null and only the leaf hash survives. The leaf, its neighbours' proofs and the chain still verify — there is simply no content left to hash. A tombstone keeps (stream, sequence, leafIndex, eventPublicId, leafHash, linkHash, checkpointId) for the life of the log.

A tombstone is the log keeping its promise, not breaking it: the retention rule deleted the data and the log still cannot lose or reorder the slot it occupied. A verifier that treated a tombstone as tampering would be punishing the platform for honouring a deletion.

And proof: null with state: "recorded" is a third, more ordinary case: the record is in the log but the next seal has not run. Ask again after the next checkpoint.

Refusals

PROOF_RECORD_NOT_FOUND404
"There is no sealed record with that id."
AUDIT_EVENT_NOT_FOUND404
No such event.
AUDIT_PROOF_NOT_SEALED409
Recorded, but not yet in a checkpoint — or not in the checkpoint you pinned.
AUDIT_CHECKPOINT_NOT_FOUND404
No checkpoint with that sequence.
AUDIT_PROOF_RANGE_INVALID400
A sequence or range that is not a sequence or range.
AUDIT_PROOF_INCONSISTENT500
The stored tree did not produce a valid proof. This is treated as an integrity failure on our side, not a bad request on yours.
Install it and check a bundle
npm install --global @vn.unifyid/integrity-verifier
# or pin the release you reviewed: @vn.unifyid/[email protected]

unifyid-integrity-verify proof unifyid-proof.json --keys keys.json

--keys takes the key set you archived rather than fetching one, for the reason Verify a checkpoint yourself gives. The on-chain checks need --rpc-url, a Base endpoint you choose rather than one of ours (for example https://mainnet.base.org, chain id 8453). Until anchoring starts at launch they are undecided — exit 3.

What this does not do

  • You do not fetch a bundle. It belongs to the person whose record it is, behind their access token, with Cache-Control: private, no-store. If you need one, they hand it to you.
  • Checks 9 and 10 do not pass yet. Every published description of the verification chain lists ten steps; eight of them are reachable today, and the last two from launch. A report that stops at check 8 is complete, not truncated.
  • A proof proves sealing, not truth. It shows a record is the one UnifyID sealed and has not changed since. Whether what the record says is accurate is a different question, and no hash answers it.
  • Per-stream sizes leave with the proof. streamHeads tells its holder how many events each stream held at that checkpoint. That is not public, and handing a proof to somebody hands them that too.

Verified against API commit 234dba22 on 2026-10-18. The routes, scopes, checklist, events and limits on these pages are read from that export, not typed.