The seal hash
Publishing this is the point. A sealed ledger you have to take on faith is a database with better marketing.
The algorithm
Section titled “The algorithm”recordHash = SHA-256( UTF-8( canonicalDocument ) ) → lowercase hexwhere canonicalDocument is a JSON object with these keys, in this order, PascalCase:
{"EntityType":"VoucherInstance:events","EntityId":"<32 hex, no dashes>","CanonicalPayload":"<the stored payload string>","PreviousHash":"<64 hex>","SequenceNumber":1}Events sealed with an actor use the same shape plus two more keys at the end:
,"ActorConnectedId":"<32 hex>","ActorIsEmployee":trueEntityType is the anchor namespace — "{type}:events", not the bare type. The API returns
it as anchorNamespace so you never have to construct it.
The trap
Section titled “The trap”The server serializes with .NET’s System.Text.Json default encoder, which does not
agree with JSON.stringify.
Escaped as \uXXXX with uppercase hex, even though most are ASCII-safe:
| character | becomes |
|---|---|
" | " |
& | & |
' | ' |
+ | + |
< | < |
> | > |
| backtick | ``` |
| DEL (0x7F) | |
| every non-ASCII character | \uXXXX per UTF-16 code unit |
Not escaped, though people assume they are: = and /.
Kept in short form: \\, \b, \f, \n, \r, \t.
Two consequences that catch every first attempt:
프로becomes프로, not the literal characters.- 🎉 becomes
🎉— a surrogate pair, two escapes, not one.
A naive implementation passes every ASCII test and then reports a genuine voucher as tampered the first time a product is named in Korean, or a note contains an ampersand.
Conformance vectors
Section titled “Conformance vectors”sdk/ts/test/vectors.json in the repository is generated from the real C# hasher and
asserted by both implementations. sdk/ts/test/ascii-escape-map.txt records exactly
which printable-ASCII characters the encoder escapes — measured over the whole range, not
remembered.
If you implement this in another language, run against those vectors. If they fail, your implementation is wrong; the vectors are not a snapshot to be regenerated.
Legacy rows
Section titled “Legacy rows”Events older than canonPreservedSinceUtc (2026-08-02 11:00 UTC) lost their canonical
string to a jsonb column that normalised the JSON on insert. They can never re-hash.
Report them legacy, never tampered — see
Verify the seal yourself.