La documentación se publica en inglés.

DocumentationTrust

What we publish

One signed statement an hour, over combined roots only, with no personal data in it — and why the numbering is not one per hour.

The hourly seal

A scheduled job named audit-checkpoint runs on the cron pattern 5 * * * * — once an hour, at five minutes past. It reads the head of every sealed stream, combines them into one root, builds a 256-byte statement and signs it. The signing never waits for anything else.

Sequences themselves have no gaps. Each statement's sequence is the previous one's plus one, its periodStart is the previous one's periodEnd, and its previousCombinedRoot is the previous one's combinedRoot. That continuity is checked when the statement is sealed and again by a database trigger.

Five streams, one combined root

The log is five append-only streams, each an RFC 6962 Merkle tree over its own events. A statement commits to all five at once.

The sealed streams, in the order the combined root hashes them

audit
What happened on the platform: the actions the system and its people took.
access
Reads of personal data by somebody other than the person it belongs to.
disclosure
Personal data released outside UnifyID. Every release to your application lands here.
receipts
Reserved. It held agent approvals and the receipts for committed agent actions, and it is empty now that agents are gone; the stream stays because its hash chain and its checkpoints do.
transparency
Public facts: legal notice versions and signing key rotations.

Each stream's leaf is salted. A leaf is SHA-256(0x00 || leafSalt || contentHash), where leafSalt is 32 random bytes the database assigns when the event is stored. The salt is the reason a guessed record — "person X connected application Y at time T" — cannot be confirmed against a published root or against a proof somebody else shared.

The combined root, from the five stream heads
combinedRoot = SHA-256( 0x02 || H(audit) || H(access) || H(disclosure) || H(receipts) || H(transparency) )

H(stream) = uint8(len(name)) || name (ASCII) || uint64_be(treeSize) || rootHash (32 bytes)

All five are always present. An empty stream is treeSize = 0 with the RFC 6962 empty-tree hash. The 0x02 prefix separates a combined root from a leaf (0x00) and an interior node (0x01), so no one byte string can be read as two different things.

The statement is 256 bytes

A checkpoint statement is the Solidity ABI encoding of eight static words — eight 32-byte fields, no offsets, no length prefix, exactly 256 bytes. Version 1 is the only version, and a decoder refuses any other value or any non-zero padding.

Statement version 1. Integers are big-endian and left-padded with zeros.

BytesFieldWhat it is
0..31version1.
32..63logIdSHA-256("unifyid-audit-log:" + <log name>). It separates production, staging and a local log, so a statement from one can never be read as a statement from another.
64..95sequenceThe checkpoint index: 1, 2, 3 and so on, with no gaps.
96..127periodStartUnix seconds. Equal to the previous statement's periodEnd; 0 for sequence 1.
128..159periodEndUnix seconds when the stream heads were read. Always greater than periodStart.
160..191combinedRootThe root above.
192..223previousCombinedRootThe previous statement's combinedRoot; 32 zero bytes for sequence 1.
224..255signingKeyFingerprintSHA-256(0x04 || X || Y) of the P-256 public key that signed this statement.

The fingerprint of a checkpoint is SHA-256(statement). It is also the digest that gets signed, which means the fingerprint and the signed message are the same 32 bytes — there is no separate thing to check.

The signature

Algorithm
ECDSA P-256 over SHA-256(statement) — JOSE ES256. Signed once when the statement is sealed, stored, and never re-signed.
Encoding
64 bytes, r || s, each 32 bytes big-endian. In JSON it is 0x and 128 hex characters. Not DER, not JWS.
Low-S
s is normalised low (s <= n/2). A high-S signature is mathematically valid and is refused anyway, so each statement has exactly one accepted signature encoding.
Key custody
Production signs in AWS KMS: the API refuses to start unless AUDIT_CHECKPOINT_SIGNER is kms, and the private key never leaves KMS. The bytes are identical either way.

Keys are listed newest first, one active at a time. A new key retires the previous one, and a retired key can never sign again — sealing refuses with AUDIT_SIGNING_KEY_RETIRED. A configured rotation writes platform.signing_key.rotated into the transparency stream inside the sealing transaction, before the heads are read, so the first statement the new key signs also seals the record of its own arrival.

What is deliberately not published

The public statement carries the combined root and nothing else about the trees underneath it.

  • Per-stream roots and tree sizes are private. How many events each stream holds is not a public number. They travel inside a proof, to the holder of that proof, who needs them to recompute the combined root.
  • No personal data is ever in a checkpoint. A statement is roots, a sequence, two timestamps and a key fingerprint. Nothing in those 256 bytes names a person, and there is no per-person hash in them either.
  • No event content. A leaf is a salted hash. The content it hashes stays in the database, under the ordinary access rules.

This is why the published feed can be genuinely open: there is nothing in it to protect, and an adversary who reads every statement ever issued learns the shape of nothing.

What this does not do

  • A checkpoint does not say a record is true. It says a record is the one UnifyID sealed and that it has not changed since. Those are different claims, and only the second one is cryptographic.
  • Sequences are not hours. Do not build a monitor that expects one new checkpoint every hour. Expect a monotonic sequence with no gaps and periods that join end to end.
  • An event is not provable the moment it happens. It waits for the next seal. Until then a proof request for it is refused with AUDIT_PROOF_NOT_SEALED, status 409.
  • Nothing here is anchored to a chain. Everything on this page is UnifyID signing its own statements. What that is worth, and what it is not, is What this does not yet prove.

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.