# epcis.dev — Post the event. The door does the rest.

> In EPCIS, the Who is a company. Every event has a performer. The barcode that could not say which one, and the record that cannot say who, are the same defect at two different grains. Grain is the size of the thing a record is about. A company is one grain. A batch is finer. A single unit — this case, and not the one behind it — is the finest there is. EPCIS is GS1's standard for supply-chain events — what, when, where, why, how — and its Who is a company: the GLN of the legal entity involved. This door is the capture gateway for it: three calculators answer on this origin with no key, and the write half validates, stamps, hashes and appends.

**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/.

`npx epcis.dev` is the one-line local door — translate, validate, hash, capture, MCP — the same engine this origin hosts at POST /translate, /validate and /hash.

## One specimen, three verbs — resolved by the build that published this page

A receiving event in EPCIS 1.2 XML, run through the three calculators by the same bundle `npm i epcis.dev` installs. If any answer moves, the build fails and the page does not ship.

| # | verb | against | answer |
|---|---|---|---|
| 01 | translate | EPCIS 1.2 XML → 2.0 JSON-LD | round-trip-clean · 0 lossy paths |
| 02 | validate | GS1's official 2.0.1 JSON Schema, pinned | conforms · sha256 0f46ff694efffd8d8ce840a33dfde84228add11b516b8b258f3200740ae210af |
| 03 | hash | CBV 2.0 §8.9 event identity | ni:///sha-256;d2f4ce56044660b70ca86883f1ce4b61f4c3ae13c25a5378373dd4cff1b203da?ver=CBV2.0 |

Reproduce row 03. No key, no account:

```sh
curl -sS https://epcis.dev/hash \
  -H 'content-type: application/json' \
  -d '{"type":"ObjectEvent","eventTime":"2026-08-06T14:20:07.000Z","eventTimeZoneOffset":"+00:00","epcList":["urn:epc:id:sgtin:0614141.107346.2018"],"action":"OBSERVE","bizStep":"receiving","disposition":"in_progress","readPoint":{"id":"urn:epc:id:sgln:0614141.00777.0"},"bizLocation":{"id":"urn:epc:id:sgln:0614141.00888.0"}}'

{"hashes":["ni:///sha-256;d2f4ce56044660b70ca86883f1ce4b61f4c3ae13c25a5378373dd4cff1b203da?ver=CBV2.0"]}
```

## The eight laws

1. **Stamped by the gateway, not the sender.** recordTime, spine:capturedBy and spine:attestationGrade are applied at the door. Whatever you sent in those fields is stripped. The scriber is not the part being scribed.
2. **Two grains, never collapsed.** who is the attested observer — a human, an agent, or an embodied agent — asserted by the caller and graded by the door. capturedBy is the warrantor: the account whose key opened the door, stamped by the gateway. Never the same thing, never merged; one is stamped, the other is graded. Party and org grain are derived at read time from grant chains, never stamped on an event — the record survives your reorg and your revocations.
3. **Conforms to EPCIS 2.0 and CBV 2.0.** Implemented against GS1's official OpenAPI description, pinned by digest and re-checked in CI. The spine speaks What/Who/When/Where/Why/How natively; EPCIS 2.0 is its projection, and the projection always validates.
4. **Validated on every capture.** Each event is machine-checked against the official GS1 EPCIS 2.0 JSON schema before it is accepted. A capture that does not validate is refused as RFC 7807 problem+json, carrying the standard's own exception types — not a vendor error string. The digests are on the pins page.
5. **Identified by the standard's own hash.** The standardized EPCIS event hash from CBV 2.0 §8.9, with GS1 Digital Link normalization, checked against the pinned reference vectors — so the same event captured twice is the same event, and a changed event is a different one.
6. **Append-only, by construction.** No service identity anywhere holds an UPDATE or DELETE grant; the write path has exactly one writer. You can add an event. You cannot un-write one. Corrections are declared events, never edits.
7. **Minimally scoped on read.** You see your own scope. What is outside it is absent, not greyed out and not redacted — there is no shape left behind to argue about.
8. **Ten years of vendor XML, translated.** EPCIS 1.1, 1.2 and 2.0 XML in; EPCIS 2.0 JSON-LD out; a round-trip fidelity report per job, so the translation is reviewable rather than trusted. The translation door is a page of its own.

What the laws add up to: Everyone in this market has built a network that is authoritative because you joined it. The spine makes a record that is verifiable whether or not you joined anything — and it carries, in the record itself, an attested answer to who observed the event, including when the observer was an agent.

## A refusal is a result

There is no error banner in this interface. A document that fails the schema comes back as a verdict naming the pinned digest that judged it and a JSON Pointer at the field:

```json
{"valid": false, "errors": [{"path": "/epcisBody/eventList/0/eventTime", "keyword": "format", "message": "must match format \"date-time\""}]}
```

A malformed body — broken JSON, the wrong media type, a payload over the cap — comes back as RFC 7807 application/problem+json carrying the standard's own exception type (epcisException:ValidationException, epcisException:UnsupportedMediaTypeException, epcisException:CaptureLimitExceededException). Every `type` URI dereferences to a page under /errors/.

## Where it stops

| ceiling | value | what happens at it |
|---|---|---|
| payload | 6 MiB (6,291,456 bytes) | 413 · epcisException:CaptureLimitExceededException |
| auth, calculators | none | /translate, /validate, /hash take no key and no account |
| auth, the write half | capture key | capture, query and MCP on api.epcis.dev; issuance open (P0-18) |
| rate limit | none published | no published request ceiling stands on the three calculators today |
| price | $0 per event | /pricing, declared `binding: false` — stated intent, not terms (P0-15) |
| retention | nothing retained | the three calculators store nothing; /what-we-log is the disclosure |

## The second Who

EPCIS answers what, when, where, why and how, and its Who is the GLN of a legal entity — org grain, and correct. A company has no hands, so id.org.ai (Agent. Human. Thing.) names the performer beneath the party. That is an extension, not a fork, and it is checkable: the same event carrying `spine:who` validates `true` against the same pinned GS1 schema and hashes to ni:///sha-256;05d248fa2169e6bc47247bb1b51518f288c87df5d875dc32ea147d4806ed9312?ver=CBV2.0.

capturedBy is the warrantor — the account whose key opened the door — and the gateway STAMPS it, together with recordTime and the attestation grade: caller-supplied values in those three fields are stripped and replaced. who is the attested observer — a human, an agent, or an embodied agent — asserted by the caller and GRADED by the door, reaching only the grade `claimed` until identity attestation lands. The two never collapse. Party and org grain are derived at read time from grant chains, never stamped on an event.

## The MCP door (one door for people and agents)

| tool | what it does | readOnlyHint | state |
|---|---|---|---|
| capture | POST an EPCIS document through the gateway laws: strip → validate → project-check → stamp → append | false | shipped |
| query | SimpleEventQuery over whatever was captured | true | shipped |
| get_event | one event, by its CBV §8.9 hash identity | true | shipped |
| translate | EPCIS 1.1 / 1.2 / 2.0 XML → 2.0 JSON-LD, with a per-job fidelity report | true | shipped |
| resolve | refuses honestly: returns a typed tool error, never a fabricated resolution — identity resolution lives at id.org.ai (Agent. Human. Thing.) | true | honest refusal |
| subscribe | refuses honestly: a typed tool error, never a silent downgrade to a one-shot pull | false | honest refusal |

The endpoint is POST /mcp on this origin, and npx epcis.dev mcp runs the same server on your bench.

## $0 per event — intent, not terms

Our intent is that recording an event never costs money and is never shown as a meter. Until published terms bind it, that is a commitment we make, not a contract you hold. Revenue attaches to answers on the executive side (visibility.cloud).

## Shipped / open (condensed)

Shipped:
- Hosted on this origin: POST /translate, POST /validate and POST /hash — the stateless engine doors, no key required
- Validation against the official GS1 EPCIS 2.0 JSON schema, on every capture
- Gateway stamping, with caller-supplied values stripped
- An MCP door: capture / query / get_event / translate as tools

Open in the launch ledger:
- Issued capture keys — the key list is open and the hosted door now answers it; issuance waits on the notify path and a named sender (P0-18)
- The public repository host — the clone URL ships on this site the day the publication ruling lands (P0-24)
- The Iceberg write path (Pipelines → R2 Data Catalog, R2 SQL reads) — the hosted spine writes the same rows, on the same daily partitions, straight to R2 today; the stream that would carry them is refused by the account's Pipelines quota (code 1017, 20 of 20 streams used), so the cutover is a backfill behind a limit increase (P0-14)
- The business-transaction layer’s EDIFACT track — parse of EDIFACT answers a typed SYNTAX_UNSUPPORTED refusal while this row is open (biztx ledger, EDIFACT P6)

The ledger in full, dated: /what-ships-today/

## Doors

- /translate/ — EPCIS 1.1/1.2/2.0 XML to 2.0 JSON-LD with a per-job fidelity report
- /conformance/ — the pinned GS1 digests and the advisory suite (no attestation has ever been issued)
- /mcp/ — the agent door
- /run-it-yourself/ — the five-minute local capture transcript
- /what-ships-today/ — the dated ledger
- /get-a-key/ — the capture-key waitlist (machine twin of the interview: /flow.json)
- /llms.txt · /icp.json · /agent-classes.json · /.well-known/agents.json — the machine face
