Status: Draft v0.1 · An open, vendor-neutral specification · License: open (see §10)
OTEP is an open, vendor-neutral protocol for the lifecycle of any trackable subject. It defines one event model and one status vocabulary so that tracking events from any source — own-fleet delivery, third-party couriers, carrier labels and beyond — can be exchanged, understood and projected to international standards without re-integrating for every party.
This document is the specification: the structure, the fields, the status tables, the state machine, the external-standard mappings, and the conformance rules for building a compliant implementation. It is implementation-agnostic — it describes the protocol, not any particular vendor's internals. RFC-2119 keywords (MUST / SHOULD / MAY) are normative.
Tracking is fragmented: every carrier names fields and status codes differently, every delivery channel reports in its own shape, and connecting to global standards means re-integrating again and again. OTEP gives producers and consumers a single common language: a producer emits OTEP events once, and every OTEP consumer understands them and can project them to the standard it needs.
A timeline is an envelope carrying the tracked subject and an ordered list of events.
{
"otep_version": "0.1",
"profile": "parcel", // domain profile (§8)
"subject": {
"tracking_number": "SR123...", // ≥1 identifier required
"order_id": 12345, // optional
"package_id": 67890, // optional
"external_tracking_number": "1Z...",// optional
"gs1_sscc": "00...", // optional — enables EPCIS epcList
"piece_id": "..." // optional — enables ONE Record linkage
},
"current_status": "delivered", // projection of the latest event
"delivered": true,
"events": [ /* OTEP events, §4 */ ]
}
{
// WHAT — carried once at the timeline level (subject above)
// WHEN
"occurred_at": "2026-06-10T09:30:00-04:00", // event instant, ISO-8601 with offset
"recorded_at": "2026-06-10T09:45:23-04:00", // ingestion instant (optional)
"time_type": "actual", // actual | estimated | scheduled
// WHERE
"location": {
"name": "Toronto Hub",
"code": "YYZ-2",
"gln": "0614141000005", // GS1 GLN → EPCIS SGLN
"lat": 43.6777, "lng": -79.6248,
"country": "CA" // ISO 3166-1 alpha-2
},
// WHY / WHAT HAPPENED
"status_code": "out_for_delivery", // an OTEP status code (§4)
"phase": "out_for_delivery", // derived from status_code
"incident_reason": null, // an OTEP incident reason (§4) when exception
// WHO
"actor": { "type": "driver", "name": "Jane D.", "phone": "+1..." },
// PROVENANCE
"source": {
"type": "self_delivery", // self_delivery | third_party_delivery | carrier_label
"provider_id": null,
"carrier_code": null,
"external_event_code": null, // your raw status code (preserve it)
"raw": { /* original payload */ }
},
// PROOF
"pod": {
"photos": [ { "url": "...", "content_base64": null } ],
"signature": [ { "url": "...", "content_base64": null } ],
"recipient": "John Smith"
}
}
Every field except subject, occurred_at, status_code and source is optional —
partial sources fill what they have. Node/hub scans set location to the scanning facility.
High-frequency GPS telemetry is NOT an OTEP event; an event is emitted on a status change or
a node scan.
20 canonical lifecycle codes. phase is always derivable from the code.
| code | phase | terminal | POD | meaning |
|---|---|---|---|---|
information_submitted |
pre_shipment | order info received | ||
booking_confirmed |
pre_shipment | carrier/booking acknowledged | ||
awaiting_pickup |
pre_shipment | ready for collection | ||
out_for_pickup |
pickup | en route to pick up | ||
picked_up |
pickup | ✓ | collected from shipper | |
pickup_failed |
exception | pickup attempt failed | ||
pickup_rescheduled |
exception | pickup will retry | ||
received |
inbound | received at facility | ||
arrival_scan |
inbound | arrival scan at node | ||
in_transit |
transit | moving | ||
package_outbound |
transit | departed facility | ||
removed_from_route |
exception | taken off route | ||
route_cancelled |
exception | route cancelled | ||
out_for_delivery |
out_for_delivery | on the vehicle | ||
delivered |
delivered | ✓ | ✓ | delivered to consignee |
delivery_failed |
exception | delivery attempt failed | ||
delivery_rescheduled |
exception | will retry / redeliver | ||
return_to_sender |
return | ✓ | returning to origin | |
rejected_by_recipient |
return | ✓ | recipient refused | |
cancelled |
return | ✓ | order cancelled |
pre_shipment · preparing · pickup · inbound · in_custody · transit ·
out_for_delivery · delivered · exception · return. (preparing and in_custody
are reserved for non-parcel profiles — §8.)
source.type: self_delivery · third_party_delivery · carrier_label.
time_type: actual · estimated · scheduled (default actual).
When an event is in the exception phase it SHOULD carry an incident_reason from this
normalized vocabulary:
carrier_damaged_parcel, carrier_sorting_error, carrier_address_not_found,
carrier_parcel_lost, carrier_not_enough_time, carrier_vehicle_issue,
carrier_capacity_exceeded, carrier_mechanical_delayretailer_cancelled, retailer_incorrect_data, retailer_not_ready,
retailer_incorrect_parcel, retailer_incorrect_dimensions, retailer_packaging_issueconsignee_refused, consignee_business_closed, consignee_not_available,
consignee_not_home, consignee_cancelled, consignee_verification_failed,
consignee_incorrect_address, consignee_access_restricted, consignee_safe_place_unavailablecustoms_delay, customs_documentation, customs_duties_unpaid,
customs_prohibited, customs_inspectionweather_delay, natural_disaster, force_majeureparcel_being_researched, security_issue, regulatory_hold, unknownPhases advance forward; exceptions interrupt and resolve back. Terminal states (delivered,
return_to_sender, rejected_by_recipient, cancelled) close the subject.
pre_shipment → preparing → pickup → inbound → in_custody → transit → out_for_delivery → delivered ✓
└──────────── exception ──────────┘
└──────────── return / cancelled ✓
Rules:
occurred_at and MUST tolerate out-of-order arrival.OTEP's four dimensions line up field-for-field with the major standards, so each is an output projection of an OTEP event.
| OTEP | GS1 EPCIS 2.0 | IATA ONE Record | UN/CEFACT |
|---|---|---|---|
occurred_at (+offset) |
eventTime + eventTimeZoneOffset |
eventDate + eventTimeType=Actual |
Event Date/Time |
recorded_at |
recordTime |
recordedAt | — |
| subject (sscc/piece) | epcList (SSCC URN) |
linkedObject → Piece/Shipment |
Consignment |
location (gln/lat/lng) |
readPoint / bizLocation (SGLN) |
recordedAtLocation → Location |
Location |
status_code |
bizStep + disposition |
eventCode |
Transport status code |
actor |
sourceList / extension | Actor / Party | Party |
incident_reason |
disposition / ErrorDeclaration | event remark | Status reason code |
Per-code values (EPCIS CBV bizStep/disposition, ONE Record eventCode, UN/CEFACT code)
are published in the machine-readable codebook. Beyond these international standards, an OTEP
timeline can also be projected to OpenTelemetry traces, OGC SensorThings observations, and
common commerce platforms (AfterShip, Shopify, Amazon, Walmart, BigCommerce, Magento,
WooCommerce, Etsy).
Confidence: EPCIS CBV values are stable standard URNs. ONE Record and UN/CEFACT code values are best-fit for last-mile and should be validated against the official code lists before external use. "Delivered to consignee" has no exact CBV bizStep — the closest fit (
receiving+received) is used, or a user-vocabulary extension URN.
OTEP is consumed over a small read-only HTTP surface; all endpoints are public.
| Verb | Path | Returns |
|---|---|---|
| GET | /api/v1/otep/trackings/{tracking_number} |
the timeline for a tracking number |
| GET | /api/v1/otep/trackings/{tracking_number}/events |
events only |
| POST | /api/v1/otep/trackings/batch |
many tracking numbers in one call |
| POST | /api/v1/otep/validate |
conformance check for a posted timeline (§9) |
A GraphQL query exposing the same timeline is also available.
The same timeline is serialized into whichever representation you request, via a ?format=
query parameter or an Accept profile:
| Request | Representation |
|---|---|
?format=otep (default) |
native OTEP timeline |
?format=epcis |
GS1 EPCIS 2.0 (JSON-LD ObjectEvents) |
?format=onerecord |
IATA ONE Record (LogisticsEvents) |
?format=uncefact |
UN/CEFACT transport status |
?format=otlp |
OpenTelemetry traces |
?format=sensorthings |
OGC SensorThings observations |
?format=aftership | shopify | amazon | walmart | bigcommerce | magento | woocommerce | etsy |
the platform's tracking/fulfillment shape |
Events that cannot be assigned a code in a projection are skipped and counted (never silently dropped).
OTEP is a general protocol, not a parcel one. The protocol layer (event envelope, phase
spine, state machine) is universal; concrete status codes belong to a profile declared on
the timeline via profile. The codes in §4 are the parcel profile. Other domains —
moving, food delivery, storage and beyond — add their own code sets under their profile,
namespaced otep:<profile>:<code>, each mapping up to the same phase spine. Adding a profile
is an extension, not a protocol change.
A producer or consumer is OTEP-conformant when every event it emits or accepts satisfies these tables and rules.
| Field | Type | Req. | Constraints |
|---|---|---|---|
otep_version |
string | MUST | semver, e.g. 0.1 |
profile |
string | MUST | a registered profile |
subject |
object | MUST | §9.2 |
current_status |
string|null | SHOULD | a status code (§4) |
current_phase |
string|null | SHOULD | MUST equal the phase of current_status if both present |
delivered |
boolean | SHOULD | true iff current_status = delivered |
events |
array | MUST | event objects (§9.3), orderable by occurred_at |
subjectAt least ONE of tracking_number / order_id / package_id MUST be present.
| Field | Type | Constraints |
|---|---|---|
tracking_number |
string | non-empty |
order_id / package_id |
integer|null | |
external_tracking_number |
string|null | |
gs1_sscc |
string|null | 18 digits |
piece_id |
string|null |
| # | Field | Type | Req. | Constraints |
|---|---|---|---|---|
| 1 | occurred_at |
string | MUST | ISO-8601 with offset |
| 2 | recorded_at |
string|null | SHOULD | ISO-8601 |
| 3 | time_type |
string | MAY (default actual) |
actual | estimated | scheduled |
| 4 | status_code |
string|null | MUST¹ | a code in §4.1 |
| 5 | phase |
string|null | SHOULD | MUST equal the phase of status_code |
| 6 | incident_reason |
string|null | SHOULD² | a reason in §4.4 |
| 7 | description |
string|null | MAY | human-readable |
| 8 | location |
object|null | MAY | §9.4 |
| 9 | actor |
object|null | MAY | { type, name?, phone? }; type ∈ {driver, operator, carrier, system} |
| 10 | source |
object | MUST | §9.5 |
| 11 | pod |
object|null | MAY³ | { photos[], signature[], recipient? } |
¹ A coded event MUST carry a status code from §4.1. A raw scan you cannot yet classify MAY set
status_code = null, but MUST preserve the native code in source.external_event_code and MUST
be counted, never dropped.
² An exception-phase event SHOULD carry an incident_reason.
³ Events with status_code ∈ {delivered, picked_up} SHOULD carry a pod.
locationname (string) · code (string) · gln (GS1 GLN, 13 digits) · lat / lng (WGS-84) ·
country (ISO 3166-1 alpha-2). All optional.
source| Field | Type | Req. | Constraints |
|---|---|---|---|
type |
string | MUST | self_delivery | third_party_delivery | carrier_label |
provider_id |
integer|null | MAY | |
carrier_code |
string|null | MAY | |
external_event_code |
string|null | SHOULD⁴ | your raw status code |
raw |
object|null | MAY | original payload |
⁴ MUST be present when status_code is null, so the native code is never lost.
occurred_at MUST parse as ISO-8601. Normalize numeric epochs and .NET /Date(ms)/
on ingest; do not emit those forms.occurred_at, tolerate out-of-order arrival,
and dedupe on (subject, status_code, occurred_at).phase, current_status, current_phase, delivered are projections — if
present they MUST be consistent with the event timeline.Verify your output by POSTing a timeline to POST /api/v1/otep/validate. Treat any
errors as blocking; address warnings. A machine-readable JSON Schema and the complete
codebook (every status code, phase and external mapping) are published for offline validation.
OTEP is an open specification, free for any party to implement.
otep:<profile>:<code>, phases as
otep:phase:<name>. Once published in a released version, an identifier's meaning is immutable.otep_version).x- prefix
(otep:parcel:x-my_code) until registered.OTEP welcomes other vendors to bring their own tracking-event standard so OTEP can interoperate with it, in either direction:
source.external_event_code; unmapped codes are counted, never dropped.Even if you cannot adopt OTEP directly, you are welcome to add a single normalized otep_status
to your own API responses and to share your tracking event codes for crosswalk mapping. Propose
mappings against this spec (rather than forking) so implementers converge; new formats plug into
the same ?format= content negotiation and never break an existing consumer.