Identity is computed, never assigned: the CBV §8.9 event hash.
31 July 2026 · the launch journal
Somewhere in your build-vs-buy spreadsheet there is a row that looks small and is not: event identity. Your system will store observations that your trading partners also store — the same pallet, scanned by your 3PL, recorded by their WMS, forwarded through a co-manufacturer's middleware. If each party assigns its own IDs, then "is your event my event?" becomes a reconciliation project: mapping tables, fuzzy matching on timestamps, a meeting. Assigned identity fails precisely at the boundary where traceability matters — between parties who share no database, no vendor, and no ID-issuing authority.
The standard's answer is the most durable kind: make identity a pure function of content. CBV 2.0 §8.9 defines a standardized event hash — canonicalize the event, concatenate its values in a specified order, sha-256 the result, express it as an RFC 6920 ni: URI. Two independent parties who observed the same facts compute the same identity, with zero coordination. No registry, no sequence, no vendor in the loop.
One event, worked end to end.
Here is the reference fixture epcisDocWithSingleEvent — an OpenEPCIS test vector, vendored in this repo. The source event, as captured:
{
"type": "ObjectEvent",
"action": "OBSERVE",
"bizStep": "shipping",
"disposition": "in_transit",
"epcList": ["urn:epc:id:sgtin:0614141.107346.2017",
"urn:epc:id:sgtin:0614141.107346.2018"],
"eventTime": "2022-02-16T20:33:31.116000-06:00",
"eventTimeZoneOffset": "-06:00",
"readPoint": { "id": "urn:epc:id:sgln:0614141.07346.1234" },
"bizTransactionList": [{ "type": "po",
"bizTransaction": "http://transaction.acme.com/po/12345678" }]
}
Step one is canonicalization, and it is where the algorithm earns its keep. The §8.9 pre-hash string for this event — the exact byte sequence that gets hashed:
eventType=ObjectEvent
eventTime=2022-02-17T02:33:31.116Z
eventTimeZoneOffset=-06:00
epcListepc=https://id.gs1.org/01/10614141073464/21/2017
epc=https://id.gs1.org/01/10614141073464/21/2018
action=OBSERVE
bizStep=https://ref.gs1.org/cbv/BizStep-shipping
disposition=https://ref.gs1.org/cbv/Disp-in_transit
readPointid=https://id.gs1.org/414/0614141073467/254/1234
bizTransactionListtype=https://ref.gs1.org/cbv/BTT-po
bizTransaction=http://transaction.acme.com/po/12345678
(Line breaks added for reading; the wire form is one unbroken string.) Look at what normalization did: the local eventTime became UTC; every urn:epc:id:sgtin: EPC became its GS1 Digital Link form; the bare CBV words became their full web-vocabulary URIs; fields were emitted in the spec's canonical order regardless of the JSON's key order. That is why a partner whose system speaks EPC URNs and a partner whose 2D-era stack speaks Digital Link compute the same identity for the same observation — the hash is over meaning, not spelling.
Step two is the hash itself:
$ npx epcis.dev hash epcisDocWithSingleEvent.jsonld
hash 0 ni:///sha-256;ffa491b16867e34a7791a8d5389d826afc24e63a121026b6a4004ace38631b4b?ver=CBV2.0
Gated against reference vectors, not our own opinion.
A hash implementation that certifies itself is worthless — any bug reproduces on both sides of the comparison. So this implementation is gated against the reference vectors published by the OpenEPCIS event-hash project: their fixture files, their pre-hash strings, their expected hashes, vendored under vendor/openepcis/event-hash/ and sha256-pinned like every other authority artefact here. The published vector for the fixture above:
$ cat vendor/openepcis/event-hash/epcisDocWithSingleEvent.hashes
ni:///sha-256;ffa491b16867e34a7791a8d5389d826afc24e63a121026b6a4004ace38631b4b?ver=CBV2.0
Byte-identical. The suite runs every vendored vector set — multi-event documents, CURIE forms, EPC-list normalization edge cases, timestamp round-off — and a mismatch anywhere is a red build. When your team evaluates any implementation of §8.9, including one you build in-house, this is the acceptance test to demand: agreement with independent published vectors, with the vectors pinned so they cannot drift to fit.
One more design fact your architects will ask about: the algorithm deliberately excludes eventID, recordTime, and errorDeclaration from the hash. Identity is the observation, not the bookkeeping — so a gateway stamping recordTime at admission does not change what the event is, and a replayed capture deduplicates against the same identity no matter which side's clock recorded it.
What computed identity buys the roadmap.
This is the row in the spreadsheet where buy quietly beats build, because everything downstream leans on it:
- Dedupe without coordination — a flaky reader or a retrying agent produces one stored event, no distributed lock, no sequence numbers.
- Integrity for free — recompute the hash at read time and any silent mutation of an append-only record announces itself.
- Cross-party reconciliation as set intersection — your events, your partner's events, one join key that neither of you issued. The trace answer becomes a verifiable chain of hashes rather than a story.
- Era-bridging — URN-speaking legacy systems and Digital-Link-speaking 2D systems land on identical identities, so the migration seam doesn't fork your history.
The maintenance tax is real — §8.9 canonicalization has genuine edge cases, and the vectors exist because implementations get them wrong — which is exactly why the implementation here ships pinned, vector-gated, and runnable on your own bench in one command:
$ npx epcis.dev hash your-event.json
Same event, same hash, forever — computed by you, verified against vectors neither of us controls. That is what identity should cost: nothing, and no one's word.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.