The blog  ·  2026-07-31

From 856 to EPCIS document: every byte accounted for

Written for ED-3 — the in-house supply-chain platform team — the coding-class agent deputized inside the engineer's session — fenced I/O it can diff.

Contract of this page: one input document, one expected output document, the mapping stated per field, the gates stated per verdict. Both fixtures are reproduced in full. An agent holding this page can diff its own compile against the expected output mechanically; nothing below requires interpretation.

Input: 856-ship-notice-01.edi

Synthetic, license-clean, from the golden join corpus. One shipment, one order reference, one SSCC pack, two lines — one lot-grain, one serialized. 4010, standard delimiters.

ISA*00*          *00*          *ZZ*ACMEFOODS      *ZZ*NORTHBUYER     *260730*1800*U*00401*000000101*0*P*>~
GS*SH*ACMEFOODS*NORTHBUYER*20260730*1800*101*X*004010~
ST*856*0001~
BSN*00*SHIP20260730001*20260730*1800~
HL*1**S~
DTM*011*20260730*1800~
REF*BM*BOL-991~
N1*SF*ACME FOODS DC*UL*0614141000012~
N1*ST*NORTH BUYER DC*UL*0724142000024~
N1*SE*ACME FOODS*UL*0614141000005~
N1*BY*NORTH BUYER*UL*0724142000017~
HL*2*1*O~
PRF*PO-8812~
HL*3*2*P~
MAN*GM*006141411234567890~
HL*4*3*I~
LIN**UP*614141123452*LT*L42~
SN1**12*EA~
HL*5*3*I~
LIN**UP*614141765430~
SN1**2*EA~
REF*SE*1001~
REF*SE*1002~
SE*22*0001~
GE*1*101~
IEA*1*000000101~

Output: 856-ship-notice-01.expected.epcis.json

{
  "@context": "https://ref.gs1.org/standards/epcis/epcis-context.jsonld",
  "type": "EPCISDocument",
  "schemaVersion": "2.0",
  "creationDate": "2026-07-31T00:00:00.000Z",
  "epcisBody": {
    "eventList": [
      {
        "type": "ObjectEvent",
        "eventTime": "2026-07-30T18:00:00.000Z",
        "eventTimeZoneOffset": "+00:00",
        "action": "OBSERVE",
        "bizStep": "shipping",
        "disposition": "in_transit",
        "epcList": ["https://id.gs1.org/00/006141411234567890"],
        "bizTransactionList": [
          { "type": "desadv",
            "bizTransaction": "urn:epcglobal:cbv:bt:0614141000012:SHIP20260730001" },
          { "type": "po",
            "bizTransaction": "urn:epcglobal:cbv:bt:0724142000017:PO-8812" },
          { "type": "bol",
            "bizTransaction": "urn:epcglobal:cbv:bt:0614141000012:BOL-991" }
        ],
        "sourceList": [
          { "type": "location", "source": "https://id.gs1.org/414/0614141000012" },
          { "type": "owning_party", "source": "https://id.gs1.org/417/0614141000005" }
        ],
        "destinationList": [
          { "type": "location", "destination": "https://id.gs1.org/414/0724142000024" },
          { "type": "owning_party", "destination": "https://id.gs1.org/417/0724142000017" }
        ]
      },
      {
        "type": "AggregationEvent",
        "eventTime": "2026-07-30T18:00:00.000Z",
        "eventTimeZoneOffset": "+00:00",
        "action": "ADD",
        "bizStep": "packing",
        "disposition": "in_progress",
        "parentID": "https://id.gs1.org/00/006141411234567890",
        "childEPCs": [
          "https://id.gs1.org/01/00614141765430/21/1001",
          "https://id.gs1.org/01/00614141765430/21/1002"
        ],
        "childQuantityList": [
          { "epcClass": "https://id.gs1.org/01/00614141123452/10/L42",
            "quantity": 12 }
        ],
        "bizTransactionList": [
          { "type": "desadv",
            "bizTransaction": "urn:epcglobal:cbv:bt:0614141000012:SHIP20260730001" }
        ]
      }
    ]
  }
}

The mapping, field by field

  • BSN00SHIP20260730001desadv reference on every event, scoped by the ship-from GLN: urn:epcglobal:cbv:bt:{shipFromGLN}:{BSN02}. The scheme is CBV 2.0 §8.5's.
  • DTM*011 (shipped date-time) → eventTime on the shipping event. Which DTM wins is pinned mapping data, not code — a journaled ruling.
  • PRFPO-8812 under HL21O → po reference; the order HL emits no event. Assigning party = buyer GLN (N1\*BY), hence …bt:0724142000017:PO-8812.
  • REFBMBOL-991bol reference, ship-from scoped.
  • N1SF/ST (GLN-qualified, N103 UL) → location source/destination as Digital Link AI 414 URIs; N1SE/BY → owning_party as AI 417. Ruling detail in the N1 post.
  • HL32P + MANGM*006141411234567890 → one AggregationEvent, parentID = the SSCC as AI 00. One event per SSCC-bearing packaging node — the tree post names this as the mapping's one ruling.
  • LIN*UP614141123452LTL42 + SN1*12EA → lot grain: childQuantityList entry, epcClass …/01/00614141123452/10/L42, quantity 12. UPC-12 normalizes to GTIN-14 by 00 prefix.
  • LIN*UP614141765430 + REFSE1001/1002 → serial grain: two childEPCs at …/01/…/21/{serial}.
  • readPoint / bizLocation: omitted, deliberately. An EDI document names transaction parties, not observation read points; omission is the honest emission, and both fields are optional in the pinned schema.
  • Why OBSERVE, not ADD, on the shipping event: a despatch advice reports that identified objects shipped; it does not commission them.

Identifier policy: all instance/class/party identifiers emit as canonical GS1 Digital Link URIs. EPC URN forms require the GS1 Company Prefix length, which a bare GTIN-14 or GLN-13 on the wire does not carry; Digital Link needs no split, and the pinned schema accepts format: uri at every join point.

Gate 1 — schema validation

Every emitted document must validate against the pinned official EPCIS 2.0 JSON schema, sha256 0f46ff694efffd8d8ce840a33dfde84228add11b516b8b258f3200740ae210af (retrieved 2026-07-19; digests published at epcis.dev/conformance). The acceptance oracle for the join verb is epcis.dev's pinned validator — exit 0, or the compile does not ship. Conformant always; a superset only where the source document carries more than the standard models, and never one without the other.

Gate 2 — event hashing

Identity is computed, never assigned: each emitted event must be hash-stable under CBV 2.0 §8.9 canonicalization (the epcis.dev hash verb, cross-checked against the OpenEPCIS reference vectors the family pins). Two compiles of the same document yield the same event hashes, or the run fails. This is what makes the corpus a regression oracle rather than a snapshot: expected.epcis.json matches byte-for-byte modulo creationDate, and hash-equal always.

Failure modes are typed, never prose

A malformed input produces no events and a machine-matchable refusal. The codes are identical for the X12 and canonical-JSON syntaxes:

HL_ORPHAN_PARENT   an HL02 references a node that does not exist
HL_CYCLE           the parent pointers do not form a tree
HL_LEVEL_ORDER     a level code appears above its legal parent
PARTY_NOT_GLN      N103 is not UL — no source/destination minted
GLN_CHECK_DIGIT    mod-10 failure — emitted, with warning

The last two are warnings, not refusals: party structure is validated, GS1 membership never. A broken tree, by contrast, refuses — a custody hierarchy with a dangling pointer is not evidence.

Same entries, second syntax

The API-native intake exercises the identical mapping: the corpus twin tx-desadv-01.json (biztx-canonical v0.1, the published JSON Schema) declares the same shipment as structured JSON — same references, same parties, same hierarchy — and compiles through the same join-map entries to the same event shapes. One law for both syntaxes; EDI included, never EDI-only.

Downstream of the skeleton: the GTIN join attaches line context at instance grain, and reconcile turns document-versus-events into a typed verdict. To run this against your own corpus, the door is get-access — state your document mix; the flow routes the rest.


848 words. The access flow is at /get-access/; the family's event door is epcis.dev.