Validate & hash
Two calculators, one discipline. /validate answers a verdict against the pinned official GS1 EPCIS 2.0.1 JSON schema — the response names the artefact and its sha256, so two parties disagreeing about validity are disagreeing about bytes, not versions. /hash computes the CBV 2.0 §8.9 standardized event hash — identity is computed from the event's content, never assigned by a database.
POST /validate
| contract | value |
|---|---|
| Method, path | POST https://epcis.dev/validate |
| Auth | none — no key, no account |
| Price class | free |
| Request | application/json or application/ld+json: an EPCISDocument or a bare EPCIS event object |
| 200 response | { valid, schema: { artefact, version, sha256 }, errors: [{ path, keyword, message }] } |
| Response headers | GS1-EPCIS-Version, GS1-CBV-Version, GS1-Extensions |
| CLI twin | npx epcis.dev validate <file.json> [--project] |
| Library twin | import { validateEpcis, getEpcisValidator, getQueryValidator, project } from "epcis.dev" |
A negative verdict is a 200 with valid: false and per-path errors — the calculator worked; the document failed. The typed refusals below are reserved for requests the door cannot judge at all.
| status | type | meaning |
|---|---|---|
| 400 | epcisException:ValidationException | malformed JSON |
| 413 | epcisException:CaptureLimitExceededException | payload exceeds the file-size limit |
| 415 | epcisException:UnsupportedMediaTypeException | this door accepts JSON; POST XML to /translate |
POST /hash
| contract | value |
|---|---|
| Method, path | POST https://epcis.dev/hash |
| Auth | none — no key, no account |
| Price class | free |
| Request | application/json or application/ld+json: a single event, an array of events, or an EPCISDocument with epcisBody.eventList |
| 200 response | { algorithm, count, hashes: ["ni:///sha-256;…?ver=CBV2.0"] } |
| Response headers | GS1-EPCIS-Version, GS1-CBV-Version, GS1-Extensions |
| CLI twin | npx epcis.dev hash <file.json> |
| Library twin | import { eventHash, canonicalJson, preHashString } from "epcis.dev" |
| Typed refusals | 400 ValidationException (malformed JSON, or no EPCIS events found) · 413 CaptureLimitExceededException · 415 UnsupportedMediaTypeException |
The §8.9 algorithm itself excludes eventID, recordTime and errorDeclaration, so a gateway filling those in does not change the hash. Send events as they are — do not pre-strip anything.
Proof — both doors, one fixture
Executed against production with the shipped fixture golden-corpus/valid-standard/object-event-shipping.json:
$ curl -sS -X POST https://epcis.dev/validate \
-H 'content-type: application/json' \
--data-binary @object-event-shipping.json
{
"valid": true,
"schema": {
"artefact": "epcis-json-schema.json",
"version": "2.0.1",
"sha256": "0f46ff694efffd8d8ce840a33dfde84228add11b516b8b258f3200740ae210af"
},
"errors": []
}
$ curl -sS -X POST https://epcis.dev/hash \
-H 'content-type: application/json' \
--data-binary @object-event-shipping.json
{
"algorithm": "CBV 2.0 §8.9 event hash",
"count": 1,
"hashes": [
"ni:///sha-256;e5624abcce4b2fc226743e8891f69c12a07ad508953749d9b9ede6771424f91e?ver=CBV2.0"
]
}
The canonicalization trace
The hash is sha-256 over a deterministic pre-hash string — §8.9's canonical ordering of the event's fields with CBV values expanded to their web-URI forms. The library exposes the intermediate steps so you can audit any hash to the byte. The opening of the pre-hash string for the same event, from preHashString(event):
eventType=ObjectEventeventTime=2026-07-19T10:30:00.000Z
eventTimeZoneOffset=+00:00epcListepc=https://id.gs1.org/01/1061414107346
4/21/2018action=OBSERVEbizStep=https://ref.gs1.org/cbv/BizStep-shipping…
(One unbroken string on the wire; wrapped here for the page.) Same event, same bytes, same ni URI — on this door, in npx epcis.dev hash, and in any other §8.9 implementation, including the OpenEPCIS vectors.
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.