# 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:

```json
{
  "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 in [RalphTro/epcis-event-hash-generator](https://github.com/RalphTro/epcis-event-hash-generator) — Ralph Troeger's reference implementation of CBV 2.0 §8.9, and he co-chaired the workgroup that ratified it: his fixture files, his pre-hash strings, his expected hashes, vendored under `vendor/epcis-event-hash/` and [sha256-pinned like every other authority artifact here](/journal/pinned-or-it-didnt-validate/). The published vector for the fixture above:

```
$ cat vendor/epcis-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](/journal/capture-once-replay-anything/) 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](/mcp/) 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.

---

**Proof, not adjectives.** This build could not verify the gateway's test suite green, so no sentence on this page claims a passing run — the last verified run is recorded in the repository's own CI, not here. Live on this origin, no key: POST /translate, /validate, /hash. No conformance attestation has ever been issued. The dated ledger is /what-ships-today/.
