# From 856 to EPCIS document: every byte accounted for

> transactions.dev · by visibility.cloud — 2026-07-31
> Written for 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.

```text
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`

```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

- `BSN*00*SHIP20260730001` → `desadv` 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.
- `PRF*PO-8812` under `HL*2*1*O` → `po` reference; the order HL emits no
  event. Assigning party = buyer GLN (N1\*BY), hence
  `…bt:0724142000017:PO-8812`.
- `REF*BM*BOL-991` → `bol` reference, ship-from scoped.
- `N1*SF/ST` (GLN-qualified, N103 `UL`) → `location` source/destination
  as Digital Link AI `414` URIs; `N1*SE/BY` → `owning_party` as AI `417`.
  Ruling detail in [the N1 post](/blog/n1-loops-are-your-sourcelist/).
- `HL*3*2*P` + `MAN*GM*006141411234567890` → one AggregationEvent,
  `parentID` = the SSCC as AI `00`. One event per SSCC-bearing packaging
  node — [the tree post](/blog/hl-loop-to-aggregation-events/) names this
  as the mapping's one ruling.
- `LIN**UP*614141123452*LT*L42` + `SN1**12*EA` → lot grain:
  `childQuantityList` entry, epcClass `…/01/00614141123452/10/L42`,
  quantity 12. UPC-12 normalizes to GTIN-14 by `00` prefix.
- `LIN**UP*614141765430` + `REF*SE*1001/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](https://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:

```text
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](/blog/the-gtin-is-the-join-key/) attaches line context at instance
grain, and [reconcile](/blog/the-asn-said-48-the-dock-saw-47/) turns
document-versus-events into a typed verdict. To run this against your own
corpus, [the door is get-access](/get-access/) — state your document mix;
the flow routes the rest.

---
Access: https://transactions.dev/get-access/ · All posts: https://transactions.dev/blog/ · Machine face: https://transactions.dev/llms.txt
