Step-by-Step Guide to EPCIS 2.0 Event Formatting for DSCSA Compliance
Trading partners reject EPCIS events for the smallest of reasons: a missing timezone offset, a bizStep that is not a Core Business Vocabulary (CBV) 2.0 URI, an EPC written into a custom field instead of epcList. This page solves one exact problem — how to hand-build an EPCIS 2.0 event payload that passes the official GS1 JSON schema, survives a partner gateway, and reconstructs cleanly during an FDA audit. It is the field-by-field companion to the parent GS1 Standards Implementation guide, which establishes the identifier contract this page assumes is already correct. If you are still resolving product keys, start with how to map GTINs to NDCs for DSCSA compliance before you format a single event.
Under the Drug Supply Chain Security Act (DSCSA), Electronic Product Code Information Services (EPCIS) 2.0 is the messaging standard that carries unit-level traceability between manufacturers, wholesale distributors, and dispensers. Version 2.0 introduces native JSON-LD serialization, a REST query interface, and tighter alignment with CBV 2.0 — but the same discipline that made EPCIS 1.2 interoperable still applies: every field is a data contract, and one malformed value breaks the chain for an entire lot.
Prerequisites
- Python 3.10+ with
jsonschemafor schema validation andpydanticv2 if you model events as typed contracts before serialization. - The official GS1 EPCIS 2.0 JSON schema and JSON-LD context, downloaded from the GS1 standards portal and pinned to a local file — never fetched live at send time.
- CBV 2.0 vocabulary references for
bizStepanddispositionvalues (theurn:epcglobal:cbv:*URIs). - A resolved identifier set for every unit: a validated 14-digit GTIN
(01), serial(21), expiration(17), and lot(10), plus the SSCC(00)for logistics units and the GLN for every read point and business location. - Trading-partner GLNs and the business-transaction identifiers (purchase order, despatch advice) that DSCSA Transaction Information references.
Step-by-Step Solution
The six steps below build one event from the outside in — context first, then envelope, identifiers, business semantics, optional sensor data, and finally validation. Each step names the GS1 or DSCSA rule it satisfies.
Step 1 — Declare the JSON-LD context and namespace resolution
EPCIS 2.0 requires every payload to declare a @context that maps compact terms to their full GS1-defined URIs. This preserves RDF-level semantics while keeping the JSON lightweight, and it is what lets a partner’s parser resolve epcList and bizStep the same way you meant them. Omitting the context breaks semantic queries and invalidates the payload against the schema. Rule: EPCIS 2.0 JSON/JSON-LD binding — the shared GS1 context is mandatory for interoperable exchange.
{
"@context": "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld"
}
Step 2 — Construct the event envelope and core metadata
Every EPCIS event needs a deterministic envelope. The eventID must be globally unique (a urn:uuid: or ni:///sha-256;... value), eventTime must be ISO 8601 with an explicit offset, and eventTimeZoneOffset must match the physical location of the read. For DSCSA, bizTransactionList entries reference business-transaction types with standardized urn:epcglobal:cbv:btt: URIs, which is how Transaction Information and Transaction History are bound to the movement. Rule: DSCSA Transaction Information/History — every custody-relevant event must carry its originating business transaction.
{
"type": "ObjectEvent",
"eventID": "urn:uuid:8f2a5c1e-3b7d-4e9a-a1f4-6c24b5f03a11",
"eventTime": "2024-11-15T08:30:00.000-05:00",
"eventTimeZoneOffset": "-05:00",
"bizTransactionList": [
{
"type": "urn:epcglobal:cbv:btt:po",
"bizTransaction": "urn:epcglobal:cbv:bt:0614141000001:PO-99876"
}
]
}
Step 3 — Map EPC URIs to serialized identifiers
DSCSA mandates unit-level traceability, so every saleable unit resolves to a precise EPC URI. Serialized units use urn:epc:id:sgtin:; logistics units use urn:epc:id:sscc:. Serials must live in epcList (or childEPCs for aggregation) — never in a custom field, or the Verification Router Service and partner gateways cannot see them. Validate each URI against the GS1 EPC URI grammar before serialization so a malformed identifier never propagates. Rule: GS1 EPC Tag Data Standard — canonical sgtin/sscc URI syntax; DSCSA saleable-unit identification.
{
"epcList": [
"urn:epc:id:sgtin:0037000.030241.1041970"
],
"action": "OBSERVE"
}
Step 4 — Apply CBV 2.0 business steps and dispositions
bizStep and disposition tell trading partners what the event means. Common DSCSA pairings are urn:epcglobal:cbv:bizstep:shipping with urn:epcglobal:cbv:disp:in_transit, or urn:epcglobal:cbv:bizstep:receiving with urn:epcglobal:cbv:disp:active. The readPoint id must be a valid GLN URN — urn:epc:id:sgln:<companyPrefix>.<locationRef>.<extension>. A disposition outside CBV 2.0 triggers automated quarantine flags in downstream ERP/WMS systems. Consult the authoritative GS1 EPCIS and CBV standard for the exhaustive vocabulary. Rule: CBV 2.0 — only registered bizStep/disposition URIs are interoperable.
{
"bizStep": "urn:epcglobal:cbv:bizstep:shipping",
"disposition": "urn:epcglobal:cbv:disp:in_transit",
"readPoint": { "id": "urn:epc:id:sgln:0614141.00001.0" }
}
For an AggregationEvent, the same envelope carries a parentID (an SSCC) and a childEPCs array of SGTINs — the packaging relationship that a resilient parent-child serial mapping layer later reconstructs for inspectors.
Step 5 — Integrate sensor and extension data for cold chain and investigations
EPCIS 2.0 natively supports sensorElementList for IoT and environmental data. For temperature-controlled biologics, embed sensorReport objects with type, value, uom, and time. The type must be a registered CBV 2.0 term — for temperature, gs1:Temperature. During a suspect product investigation, use a custom namespace under the event’s extension area to attach quarantine status and corrective-action references without breaking the base schema. Rule: EPCIS 2.0 sensor extension — CBV-registered measurement types; DSCSA suspect-product recordkeeping.
{
"sensorElementList": [
{
"sensorMetadata": { "deviceID": "urn:epc:id:sgln:0614141.00002.0" },
"sensorReport": [
{
"type": "gs1:Temperature",
"value": 4.2,
"uom": "CEL",
"time": "2024-11-15T08:25:00.000-05:00"
}
]
}
]
}
Step 6 — Validate, serialize, and deploy via Python automation
Production pipelines must enforce schema validation before transmission. Use jsonschema with the official GS1 EPCIS 2.0 schema loaded from a local file or packaged resource — do not import a non-existent epcis_schema module, and do not fetch the schema at send time. Rule: EPCIS 2.0 JSON schema conformance — validate before every send so no malformed event reaches a partner.
import json
from pathlib import Path
from jsonschema import validate, ValidationError
# Load the EPCIS 2.0 JSON schema from a local file or packaged resource
SCHEMA_PATH = Path(__file__).parent / "schemas" / "epcis-2.0-schema.json"
def load_epcis_schema() -> dict:
with SCHEMA_PATH.open() as f:
return json.load(f)
EPCIS_2_0_SCHEMA = load_epcis_schema()
def validate_epcis_event(event_payload: dict) -> bool:
"""Validate an EPCIS 2.0 event against the official JSON schema.
Raises RuntimeError with a descriptive message on validation failure so
callers can log to the audit trail and halt transmission.
"""
try:
validate(instance=event_payload, schema=EPCIS_2_0_SCHEMA)
return True
except ValidationError as exc:
raise RuntimeError(f"EPCIS validation failed: {exc.message}") from exc
Wrap event generation in idempotent logic, retry with exponential backoff, and apply a cryptographic signature before transmission. FDA DSCSA guidance emphasizes audit-ready logging, so persist every emitted event with a stable hash for non-repudiation before it leaves for the Verification Router Service or a trading-partner endpoint.
Verification
Confirm the formatting is correct before the payload ever reaches a partner. A minimal test scaffold asserts that a well-formed event passes and a broken one is rejected with a structured error — not a crash:
import pytest
def test_valid_object_event_passes():
event = {
"@context": "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld",
"type": "ObjectEvent",
"eventID": "urn:uuid:8f2a5c1e-3b7d-4e9a-a1f4-6c24b5f03a11",
"eventTime": "2024-11-15T08:30:00.000-05:00",
"eventTimeZoneOffset": "-05:00",
"action": "OBSERVE",
"bizStep": "urn:epcglobal:cbv:bizstep:shipping",
"disposition": "urn:epcglobal:cbv:disp:in_transit",
"epcList": ["urn:epc:id:sgtin:0037000.030241.1041970"],
"readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"},
}
assert validate_epcis_event(event) is True
def test_missing_timezone_offset_is_rejected():
event = {"type": "ObjectEvent", "eventTime": "2024-11-15T08:30:00Z"}
with pytest.raises(RuntimeError):
validate_epcis_event(event)
Beyond unit tests, validate a sample batch against the GS1 schema from the command line, and inspect the append-only audit log to confirm each event was persisted with its hash. Cross-partner payload normalization and the broader ingestion contract are covered in schema validation and error handling; confirm your events survive that boundary, not just your own validator.
Gotchas & Edge Cases
eventTimein UTC buteventTimeZoneOffsetset to local. The two must be consistent. If you serializeeventTimeas aZ(UTC) instant, the offset still has to describe the physical read location — a mismatch corrupts any downstream local-time reconstruction. Store the offset that matches where the scan happened.- Serials smuggled into custom fields. EPCs placed anywhere but
epcList/childEPCsare invisible to the Verification Router Service Architecture and to partner queries. Keep every SGTIN and SSCC in the standard arrays. - Leading-zero and NDC-alignment drift in the GTIN. An
sgtinwhose company prefix and item reference do not resolve to a listed NDC formats perfectly but fails verification at the dispenser. Reconcile the GTIN first — see the GTIN-to-NDC mapping guide. - Idempotency-key collisions on retry. After a broker reconnect, a naive resend creates a duplicate commission for the same SGTIN. Key dedup on
(gtin, serial, eventTime)and deduplicate at the consumer. - Non-CBV disposition strings. A free-text
dispositionlike"in transit"(space, notin_transit, no URN) passes a loose JSON check but is silently dropped or quarantined by a strict partner gateway. Map every status to a canonicalurn:epcglobal:cbv:disp:URI.
Frequently Asked Questions
What is the single most common reason an EPCIS 2.0 event is rejected?
A missing or inconsistent eventTimeZoneOffset, closely followed by a bizStep or disposition that is not a registered CBV 2.0 URI. Both pass casual inspection but fail the official schema or the receiving gateway.
Do I need the JSON-LD @context if I only exchange plain JSON?
Yes. EPCIS 2.0 defines its JSON binding as JSON-LD, and the shared GS1 @context is what makes compact terms resolve identically across partners. Omitting it invalidates the payload against the standard schema.
Where do serial numbers belong in an EPCIS event?
In epcList for observation and commissioning events, or in childEPCs for an AggregationEvent. Never in a custom or extension field — the Verification Router Service and trading-partner queries only read the standard EPC arrays.
How long must formatted EPCIS event data be retained? DSCSA requires Transaction Information and Transaction Statements to remain reproducible for six years, in their original EPCIS 2.0 serialization, backed by an immutable audit log under 21 CFR Part 11.
Related
- GS1 Standards Implementation — the parent guide covering the full identifier-and-event contract.
- How to map GTINs to NDCs for DSCSA compliance — resolve the product key before you format the event.
- Verification Router Service Architecture — where formatted events are queried during partner verification.
- Schema validation and error handling — catching malformed events at the ingestion boundary.