Label printing guide¶
This guide walks 3rd-party WMS integrators (and any other planner adopting wes-contract) through how parcel, shipper, carton, and tracking labels flow between your planner and the executor. It covers the contract surface, the three print-timing modes, the nine real-world patterns that result, how scan events flow back to you, and how cancellation interacts with labels in flight.
The patterns here apply equally to FG.AI WES (the reference executor) and any other conforming WES vendor.
TL;DR¶
You own the label. The planner — your WMS — owns content, format, jurisdiction templates, carrier API integration, and the persistent label↔Order record-of-truth. The executor only tracks bindings (which label is associated with which physical container/slot/Order during execution) and surfaces scan events back to you.
You declare timing per dispatch. Each
dispatch.shipper-released.v1(and its sibling document envelopes) optionally carrieslabel_bindings[]. Each binding declares aprint_mode:pre-pick(you printed before dispatch),post-pick(executor will signal you to print at PackStation custody), orexternal(carrier-provided label already on the parcel). Absent or emptylabel_bindings[]defaults topost-pick— the modal 3PL case.Post-print is a round trip. When you declare
post-pick, the executor callsPOST /wes/v1/labels/issueat the custody point with the fulfilled-contents context. Your endpoint returns the issuedlabel_id+ carrier URI. The executor applies the label and continues.Scans surface as events. Whenever an operator or AMR scans a label or a slot, the executor emits a
confirmation.realtime.v1event withevent_type=label_scanned(orslot_attached,slot_detached,container_arrived,label_print_requested,label_voided_request,label_pool_scan). Subscribe to the same realtime stream you already use for short-pick / damaged / wrong-SKU exceptions.Cancellation voids labels automatically. When you cancel an Order, the executor atomically voids all bound labels and includes them in the
voided_label_bindings[]field of the cancellation ack. Use this to void carrier tracking numbers in one round trip.You don’t have to pick one pattern. Different Orders from the same planner can use different print modes simultaneously. The decision is per-dispatch, not per-tenant.
The framing¶
wes-contract deliberately keeps label content opaque to the executor. The executor never parses your barcode payload, never knows the jurisdiction template you chose, never talks to UPS/FedEx/SF Express. Three things matter to the executor:
The
label_idstring — an opaque identifier the executor stores and uses to look up the binding when a physical scan happensThe
bound_to_type— what the label is physically attached to (Shipper / Container / Tote / Slot / WorkOrder output / Cluster / external)The
print_mode— when the label exists physically relative to picking
Everything else — format, content, tracking number, carrier — is your responsibility. This separation is load-bearing: it’s what lets the same executor serve a UPS-integrated US 3PL, a SF Express-integrated CN 3PL, and a private-courier B2B shipper without per-customer code.
Decision tree — which pattern fits your operation¶
If your operation looks like one row, that’s your pattern. Patterns can mix across Orders in the same tenant.
You… |
Pattern |
Print mode |
Section |
|---|---|---|---|
Print one label per Order before pick (classic ecom small-parcel) |
A |
|
|
Pre-bind multiple totes to a batch of Orders for cart-batch picking (Locus, 6 River style) |
B |
|
|
Cartonize large Orders into N labeled cartons before pick |
C |
|
|
Pick into a generic tote; print the label at the pack station after pick is done |
D |
|
|
Pick into a container fixed in an AMR slot; print at pack from the slot id |
E |
|
|
Operators pull pre-printed labels from a yard pool and bind on the fly |
F |
(special) |
|
Bulk-pick then sort to label at a unit-sortation conveyor |
G |
(v1.x) |
|
Receive parcels that already have a carrier label on them (cross-dock, returns) |
H |
|
|
Operator damages a label and needs a reprint mid-flow |
I |
(operational exception) |
If you don’t fit one row cleanly, you’ll probably find your operation is a composition of two: pick-while-sort warehouses often mix B (for the most common SKUs) with D (for slow-movers).
The three print-timing modes¶
These are the contract primitives. Patterns A–I above are how the primitives compose.
print_mode: pre-pick¶
You have printed the label before dispatch. The barcode exists physically — either affixed to a tote in a yard, or sitting on a print queue waiting to be applied at induction. You include the label_id in the dispatch envelope. The executor creates a LabelBinding row with print_state=printed, ties it to the target you specified (bound_to_type + bound_to_id), and uses it for scan-to-claim lookup the moment an operator or AMR scans the barcode.
Use when:
You pre-cartonize at dispatch (Pattern C)
You pre-bind tote labels for batch picking (Pattern B)
You have a fully ecom-style WMS where every Order has its label allocated before release (Pattern A)
print_mode: post-pick¶
You will print the label at PackStation custody. The executor picks into ad-hoc containers (no pre-binding) and at the PackStation scan, calls your POST /wes/v1/labels/issue endpoint with the fulfilled-contents context (Order ID, line items, SKU + qty observed, slot id if applicable). Your endpoint returns one or more issued_labels[] with label_id + optional external_label_uri (the carrier tracking URI). The executor receives the issuance and applies the label.
Use when:
You print on demand based on actual fulfilled contents (the typical 3PL case — Pattern D)
You print based on AMR slot occupancy (Pattern E)
You generate jurisdiction-specific labels at the very last moment (label-language depends on observed destination — common for cross-border)
print_mode: external¶
The label already exists on the physical parcel before WES sees it. Cross-dock, returns, carrier-pre-labeled inbound — anything where you don’t print at WES’s prompting. You include the label_id (the barcode that’s physically present) in the dispatch envelope. The executor creates a LabelBinding row with print_state=external and uses it for scan-to-claim lookup. No print lifecycle.
Use when:
Cross-dock pass-through: shipper-provided carton labels on inbound (Pattern H)
Return-merchandise: customer’s outbound return label still on the package
Multi-leg supply chain where upstream already labeled (some 3PL B2B)
Defaulting behavior¶
If label_bindings[] is absent from the dispatch envelope, the executor defaults the Order to post-pick mode with bound_to_type=shipper. This matches the modal 3PL case where dispatch is the order release and labels print at pack. If your operation is exclusively post-print at the Shipper level, you do not need to populate label_bindings[] at all.
If you want different modes for different Orders in the same dispatch batch, populate the field per-Order — each dispatch.shipper-released.v1 event covers exactly one Order.
Pattern A — pre-print Shipper¶
The classic ecom case. Your WMS:
Allocates inventory for the Order
Selects carrier + service level
Calls the carrier API for a tracking number + label image
Prints the label (or queues for print-and-apply at induction)
Sends
dispatch.shipper-released.v1with the label_id
Dispatch envelope:
event_uuid: 0193f4e1-9fb1-7a4c-9f3c-2c1a8a1e6f01
event_type: dispatch.shipper-released.v1
sent_at: 2026-05-29T14:23:11Z
order_id: frn:wms:intl/tenant:acme:order/SO-0042
site_id: frn:wms:intl/tenant:acme:site/tokyo
shipper:
shipper_id: frn:wms:intl/tenant:acme:shipper/SH-0042
destination_zone: "TYO-OUT-3"
lines:
- { sku_id: "SKU-42", qty: 5, uom: "EA" }
- { sku_id: "SKU-99", qty: 3, uom: "EA" }
carrier: "yamato"
carrier_service: "TA-Q-BIN"
label_bindings:
- label_id: "1Z999AA10123456784"
external_label_uri: "https://carrier.example.com/tracking/1Z999AA10123456784"
bound_to_type: "shipper"
bound_to_hint: "frn:wms:intl/tenant:acme:shipper/SH-0042"
print_mode: "pre-pick"
Executor behavior:
Creates
LabelBindingrow withprint_state=printedOperator/AMR walks to the printed label, scans it
Executor’s ScanToClaim plan resolves
label_id→ binding →bound_to_id=SH-0042→ looks up the Task for that Shipper → operator/AMR claims the Task by scanningPick proceeds; tote with affixed label arrives at PackStation
PackStation scans the same label to confirm; ship
Realtime events you receive:
event_type: confirmation.realtime.v1
realtime_event_type: label_scanned
event_uuid: 0193f4e1-fa3a-7c20-9f3c-2c1a8a1e6f02
context:
label_id: "1Z999AA10123456784"
binding_id: frn:wes:intl/tenant:acme/site:tokyo:label-binding/LB-92831
scanned_at: 2026-05-29T14:31:08Z
scanned_by: frn:wes:intl/tenant:acme/site:tokyo:agent/operator-44
scan_location: frn:wes:intl/tenant:acme/site:tokyo:zone/pick-aisle-5
Two label_scanned events per Order in this pattern (one at pick claim, one at PackStation confirm). Both are informational — you don’t need to ack them; subscribe if you want pick-progress visibility.
Pattern B — pre-print Tote batch¶
Batch picking with pre-bound totes. Your WMS:
Selects N Orders for a cart batch (Karajan-emitted or planner-decided)
Allocates carrier + tracking per-Order
Prints N labels and pre-affixes each to a tote on the cart
Sends a single dispatch event per Order in the batch (each Order release-event carries one label binding bound to its tote)
Dispatch envelope for one Order in the batch:
event_uuid: 0193f4e2-1f01-7000-8000-aaaa00000001
event_type: dispatch.shipper-released.v1
order_id: frn:wms:intl/tenant:acme:order/SO-0101
shipper:
shipper_id: frn:wms:intl/tenant:acme:shipper/SH-0101
lines: [{sku_id: "SKU-42", qty: 1, uom: "EA"}]
carrier: "yamato"
label_bindings:
- label_id: "1Z999AA10101010101"
bound_to_type: "tote"
bound_to_hint: "frn:wes:intl/tenant:acme/site:tokyo:container/TOTE-CART07-A"
print_mode: "pre-pick"
batch_hint:
batch_id: "BATCH-CART07-2026-05-29"
position: 1
total_in_batch: 4
The batch_hint is advisory — it tells the executor’s planner-side cart-building decisions that these N Orders are pre-bound to a single cart. Without it, the executor may decide to batch differently. With it, Karajan will respect the pre-binding when generating the Task.
Executor behavior:
Creates 4
LabelBindingrows (one per Order)AMR loads cart with 4 pre-labeled totes; drives to pick aisles
Operator approaches cart, scans tote-A’s label → resolves to that Order’s Task in the batch → operator is now committed to Order-A while picking from this cart
Voice/screen instructs operator to pick the next SKU into Tote-A specifically (not just “next tote”)
After all 4 totes’ picks complete, AMR returns to PackStation
PackStation scans each tote’s label to confirm and ship per tote
Pattern B is the claim-by-scan flow at its most operationally direct: scanning the tote IS the operator committing to that Order’s pick path within the batch. Each scan emits one label_scanned event you can observe.
Pattern C — pre-print Multi-carton (cartonized)¶
For large Orders that ship in multiple cartons. Your WMS:
Runs cartonization — splits Order into N cartons
Allocates N carrier tracking numbers (one per carton)
Prints N labels with cartonization metadata
Sends one dispatch event carrying all N bindings
Dispatch envelope:
event_type: dispatch.shipper-released.v1
order_id: frn:wms:intl/tenant:acme:order/SO-0500
shipper:
shipper_id: frn:wms:intl/tenant:acme:shipper/SH-0500
lines:
- { sku_id: "SKU-A", qty: 50, uom: "EA" }
- { sku_id: "SKU-B", qty: 30, uom: "EA" }
cartonization:
- carton_index: 1
carton_id: "frn:wms:intl/tenant:acme:container/CTN-0500-1"
contents: [{sku_id: "SKU-A", qty: 50}]
- carton_index: 2
carton_id: "frn:wms:intl/tenant:acme:container/CTN-0500-2"
contents: [{sku_id: "SKU-B", qty: 30}]
label_bindings:
- label_id: "1Z999AA10202020201"
bound_to_type: "container"
bound_to_hint: "frn:wms:intl/tenant:acme:container/CTN-0500-1"
print_mode: "pre-pick"
- label_id: "1Z999AA10202020202"
bound_to_type: "container"
bound_to_hint: "frn:wms:intl/tenant:acme:container/CTN-0500-2"
print_mode: "pre-pick"
Executor creates 2 LabelBinding rows; OrderPool entry shows 2 containers. Picker scans either label first → claims the Task scoped to that carton’s contents → picks → repeats for the second carton (may be by same or different operator).
Pattern D — post-print Pack-on-demand¶
The modal 3PL case. Your WMS:
Sends dispatch with NO
label_bindings[](or empty array)Waits for
POST /wes/v1/labels/issuefrom the executor at PackStation custodyIssues the label at that moment based on observed fulfilled contents
Dispatch envelope (note absence of label_bindings):
event_type: dispatch.shipper-released.v1
order_id: frn:wms:intl/tenant:acme:order/SO-0900
shipper:
shipper_id: frn:wms:intl/tenant:acme:shipper/SH-0900
lines:
- { sku_id: "SKU-X", qty: 2, uom: "EA" }
carrier: "yamato" # carrier choice known; label not yet allocated
Executor behavior at PackStation custody:
Operator picks freely into any tote; tote arrives at PackStation
PackStation operator scans tote
Executor emits
label_print_requestedevent to theconfirmation.realtime.v1topic — AND calls your endpoint:
POST /wes/v1/labels/issue HTTP/1.1
Content-Type: application/json
{
"tenant_id": "frn:wms:intl/tenant:acme",
"site_id": "frn:wms:intl/tenant:acme:site/tokyo",
"label_print_request_uuid": "0193f4e3-77c1-7000-8000-c0c0c0c0c001",
"context": {
"order_id": "frn:wms:intl/tenant:acme:order/SO-0900",
"container_id": "frn:wes:intl/tenant:acme/site:tokyo:container/TOTE-D-119",
"fulfilled_contents": [
{"sku_id": "SKU-X", "qty_observed": 2}
],
"pack_station_id": "frn:wes:intl/tenant:acme/site:tokyo:pack-station/PS-3"
}
}
Your endpoint:
Verifies the fulfilled contents match the Order (or accepts short-pick reality)
Allocates carrier tracking, generates the label
Responds:
issued_labels:
- label_id: "1Z999AA10300300301"
external_label_uri: "https://carrier.example.com/tracking/1Z999AA10300300301"
bound_to_type: "container"
bound_to_id: "frn:wes:intl/tenant:acme/site:tokyo:container/TOTE-D-119"
print_payload_uri: "https://your-wms.example.com/labels/print/1Z999AA10300300301.zpl"
ack_envelope:
outcome: "ISSUED"
The print_payload_uri is optional — if present, the executor can fetch the print-ready payload (e.g., ZPL for Zebra printers) and send to the local printer. If absent, your external_label_uri is treated as the print source and the executor’s local printer integration fetches from there.
Executor applies the label and proceeds to ship.
Failure modes:
If your
/labels/issueendpoint returns 4xx, the executor treats it as a fatal print failure; the tote is held at PackStation and a supervisor alert fires.If your endpoint returns 5xx or times out, the executor retries with backoff (default 3 attempts over ~30s) using the same
label_print_request_uuidfor idempotency. Implement your endpoint with idempotency keyed onlabel_print_request_uuid— return the samelabel_idfor retries.If retries exhaust, the supervisor alert fires; you can manually retry via a supervisor-UI button or fix the upstream issue and the executor automatically resumes.
Pattern E — post-print AMR slot¶
Same as Pattern D, but the picking container is fixed in an AMR slot. The slot_id is persistent on the AMR body (printed as a label on each slot); the container in that slot is interchangeable.
Your WMS:
Sends dispatch with NO
label_bindings[](same as Pattern D)At PackStation, your endpoint receives a request that names the slot
Dispatch envelope: same as Pattern D.
PackStation flow:
AMR arrives at PackStation with N slots, each holding a tote with picked contents
PackStation operator scans the slot label, not the tote — the slot is fixed and pre-labeled at AMR registration
Executor’s SlotArtifact resolves: scanning slot → queries occupancy → finds current container → finds Task(s) routed to that slot during this trip → fulfilled contents
The POST /wes/v1/labels/issue body includes slot context:
context:
order_id: "frn:wms:intl/tenant:acme:order/SO-0901"
slot_id: "frn:wes:intl/tenant:acme/site:tokyo:robot/amr-042/slot/3"
container_id: "frn:wes:intl/tenant:acme/site:tokyo:container/TOTE-FLEX-77"
fulfilled_contents: [{"sku_id": "SKU-Y", "qty_observed": 3}]
pack_station_id: "frn:wes:intl/tenant:acme/site:tokyo:pack-station/PS-3"
Your response is the same shape as Pattern D. The executor applies the label to the container (not the slot — the slot keeps its slot-label permanently).
The slot identity is the persistent anchor. A given container goes through the slot for one trip; next trip, a different container occupies that slot. Your WMS doesn’t need to track slot_id long-term — just observe it in the issuance context for traceability.
Pattern F — pre-printed label pool, late-bind¶
A round-trip pattern: a warehouse maintains a stack of pre-printed labels with no Order assignment yet. Operators scan the next label off the stack, then bind to the current Order they’re picking. Common in some manual ops and certain B2B flows.
Your WMS:
Pre-prints a pool of labels in advance (typically a sequence of carrier-allocated tracking numbers)
Does NOT include them in any dispatch envelope (they have no Order binding yet)
Implements
POST /wes/v1/labels/bind-poolendpoint
Operator behavior:
Operator picks up a printed label from the yard pool
Operator scans the label
Executor sees an unknown
label_id(no LabelBinding row exists) → emitslabel_pool_scanevent onconfirmation.realtime.v1AND calls your endpoint:
POST /wes/v1/labels/bind-pool HTTP/1.1
Content-Type: application/json
{
"tenant_id": "frn:wms:intl/tenant:acme",
"site_id": "frn:wms:intl/tenant:acme:site/tokyo",
"scan_uuid": "0193f4e4-aaaa-7000-8000-bbbbbbbbbbb1",
"context": {
"label_id": "1Z999AA10400400401",
"scanned_by_agent_id": "frn:wes:intl/tenant:acme/site:tokyo:agent/operator-22",
"scan_location": "frn:wes:intl/tenant:acme/site:tokyo:zone/yard-induction",
"current_task_hint": null
}
}
Your endpoint:
Inspects context (operator’s known current Task or Order) and decides binding
Responds:
binding:
label_id: "1Z999AA10400400401"
bound_to_type: "shipper"
bound_to_id: "frn:wms:intl/tenant:acme:shipper/SH-1100"
external_label_uri: "https://carrier.example.com/tracking/1Z999AA10400400401"
ack_envelope:
outcome: "BOUND"
Or, if no Order is available to bind:
ack_envelope:
outcome: "DEFERRED"
reason: "operator has no current Task; await Task assignment first"
Executor creates the LabelBinding row on BOUND. Operator can now scan-to-claim and the binding is treated identically to Pattern A from there.
Operational latency note: Pattern F’s round trip is acceptable for low-throughput ops (sub-second latency targets are not required for label-pool binding). For high-throughput sortation or AMR-driven picking, Pattern A or B is much faster.
Pattern G — sortation-line late-bind¶
Bulk picking with unit-sort downstream. Pickers feed items into an induction conveyor; sortation reads each item’s SKU/serial barcode at induction, decides per-divert based on planner state, and the label prints at the divert chute when the item lands.
Pattern G v1 status: the data model + event class are ready; the SortationConveyorArtifact specialization itself ships only with v1 lighthouse customers committed to unit-sort warehousing. If you’re a 3rd-party WMS that drives sortation-line operations, contact FG.AI before assuming v1 support; you may be targeting v1.x.
Integration shape if available:
Dispatch event: no
label_bindings[]— labels are issued at sortation divert, not pre-printedPickers bulk-pick to induction without per-pick scans
Sortation conveyor scans each item’s intrinsic barcode (manufacturer SKU/serial)
Executor calls
POST /wes/v1/labels/issueper item-arrived-at-divert eventYour endpoint returns the issued label; executor’s chute printer applies
Per-item issuance volume is the operational concern: a 10k-items/day sortation line is 10k calls to your /labels/issue endpoint. Plan endpoint capacity accordingly.
Pattern H — cross-dock pass-through¶
Inbound parcel already carries its carrier label (returns; carrier-pre-labeled inbound; multi-leg supply chain handoff). No print lifecycle at WES.
Dispatch envelope:
event_type: dispatch.receiver-released.v1 # Receiver, not Shipper, for inbound
order_id: frn:wms:intl/tenant:acme:order/RC-0800
receiver:
receiver_id: frn:wms:intl/tenant:acme:receiver/RV-0800
expected_contents:
- { sku_id: "SKU-RETURN-Z", qty: 1, uom: "EA" }
label_bindings:
- label_id: "1Z999AA00CARRIER123" # whatever barcode is on the parcel
external_label_uri: "https://carrier.example.com/tracking/1Z999AA00CARRIER123"
bound_to_type: "external"
print_mode: "external"
Executor receives, creates a binding row with print_state=external, and uses it for scan-to-claim at the receiving dock. When an operator scans the inbound parcel, the ScanToClaim plan resolves to the Receiver Task.
Cross-dock and returns flows are otherwise governed by the Receiver and TransferIn flows.
Pattern I — damage reprint exception¶
An operational exception that can happen during any pattern. Label is damaged, torn, or unreadable; operator requests a reprint.
Flow:
Operator at PackStation observes damaged label; invokes
label_voided_request(via supervisor UI button or voice command)Executor emits
confirmation.realtime.v1event withevent_type=label_voided_request:
realtime_event_type: label_voided_request
context:
label_id: "1Z999AA10202020201"
binding_id: frn:wes:intl/tenant:acme/site:tokyo:label-binding/LB-92831
voided_reason: "damaged"
observed_by: frn:wes:intl/tenant:acme/site:tokyo:agent/operator-22
The executor immediately calls your reprint endpoint:
POST /wes/v1/labels/reprint HTTP/1.1
Content-Type: application/json
{
"tenant_id": "frn:wms:intl/tenant:acme",
"site_id": "frn:wms:intl/tenant:acme:site/tokyo",
"reprint_uuid": "0193f4e5-cccc-7000-8000-dddddddddd01",
"context": {
"voided_binding_id": "frn:wes:intl/tenant:acme/site:tokyo:label-binding/LB-92831",
"voided_label_id": "1Z999AA10202020201",
"voided_reason": "damaged",
"preserve_carrier_tracking": true
}
}
Your endpoint decides:
Preserve tracking (most common): same carrier tracking number, new label print; respond with the same
label_idif your carrier supports duplicate prints, OR a newlabel_idthat maps to the same tracking numberRe-allocate tracking (less common): void the carrier tracking, allocate a new one; respond with a fresh
label_id
Response:
new_binding:
label_id: "1Z999AA10202020201" # same or new — your choice
external_label_uri: "https://carrier.example.com/tracking/1Z999AA10202020201"
bound_to_type: "container"
bound_to_id: "frn:wms:intl/tenant:acme:container/CTN-0500-1"
ack_envelope:
outcome: "REPRINTED"
Executor creates a new
LabelBindingrow withsucceeded_bypointing from old (voided) to new (printed). Operator applies the reprinted label and continues.
Audit chain: the void/reprint chain is preserved in the binding rows. A historical query of the order’s labels will show both the original (voided) and the replacement (current).
Cancellation interaction with labels¶
When you cancel an Order, the executor:
Atomically voids ALL
LabelBindingrows for the cancelled Order withvoided_reason='order-cancelled'Emits per-binding
label_voided_requestevents onconfirmation.realtime.v1(event_type, NOT requesting your action — just informing)Includes the voided bindings in the cancellation ack envelope as
voided_label_bindings[]
The ack envelope from dispatch.cancelled.v1 is extended:
cancel_outcome: "ACCEPTED"
custody_confirmed_for: []
compensation_initiated_for:
- task_id: ...
order_id: frn:wms:intl/tenant:acme:order/SO-0042
compensation_task_id: ...
voided_label_bindings:
- label_id: "1Z999AA10123456784"
binding_id: frn:wes:intl/tenant:acme/site:tokyo:label-binding/LB-87231
bound_to_type: "shipper"
bound_to_id: frn:wms:intl/tenant:acme:shipper/SH-0042
external_label_uri: "https://carrier.example.com/tracking/1Z999AA10123456784"
voided_reason: "order-cancelled"
Your reconciler MUST consume the voided_label_bindings[] and:
Mark the label invalid in WMS (so it doesn’t appear in future shipping manifests)
Call the carrier’s void-tracking API for each
external_label_uri(so the carrier doesn’t bill you for never-shipped labels)Update the Order’s label set in your record-of-truth
This is the cleanest path — one round trip surfaces both the cancellation outcome and the labels needing carrier-side action.
TOO_LATE interaction: if the cancellation returns TOO_LATE (custody confirmed at receiving party — see the cancellation guide), then labels that were already at custody are NOT voided (the package is on the truck or with the customer). The voided_label_bindings[] array will only contain bindings whose custody had not yet confirmed. Your downstream reversal flow (refund / return) handles the remainder.
Carrier integration boundary¶
The contract intentionally does NOT define how you talk to UPS/FedEx/Yamato/SF Express. That is your integration; the contract surface only mediates label_id + URI.
A clean carrier integration pattern:
┌──────────────────┐ pre-print ┌──────────────┐
│ Your WMS │ ◄───────────────► │ Carrier │
│ │ allocate + │ (UPS API, │
│ - shipping │ fetch label │ FedEx API, │
│ orchestrator │ │ Yamato API)│
│ - label cache │ └──────────────┘
└──────────────────┘
│ ▲
│ │ /labels/issue /labels/reprint /labels/bind-pool
│ │ (post-print, exceptions, pool-bind)
▼ │
┌──────────────────┐
│ WES (executor) │
│ │
│ - binding rows │
│ - scan events │
└──────────────────┘
The boundary stays clean if:
Your
/labels/issueendpoint synchronously calls the carrier API and returns the issued label in one round trip (target: <2s for ecom carriers, <5s for freight)Your
/labels/reprintendpoint uses the same upstream-fetched carrier label image where possible (avoid re-allocation cost)Your cancellation reconciler is the only path that calls carrier void APIs (not executor-initiated)
Endpoints that exceed latency targets cause operator-perceptible delays at PackStation. Some integrators pre-allocate carrier labels in a hot cache to keep /labels/issue snappy; the cache + cancellation void interaction is your problem to design.
Common pitfalls¶
1. Treating label_id as a carrier tracking number internally¶
label_id is opaque. It may be the carrier tracking number, or it may be your internal label ID with the carrier tracking in external_label_uri. Don’t assume; carry both fields.
If you use the carrier tracking number as label_id directly, you lose the ability to reprint without changing the identifier (Pattern I preserve_carrier_tracking=true becomes harder). Recommend: use your internal label ID as label_id and put the carrier tracking number in external_label_uri.
2. Omitting label_bindings[] when you actually pre-print¶
If you do print labels at dispatch but forget to populate label_bindings[], the executor defaults to post-pick mode and will call your /labels/issue endpoint at PackStation. Your printer queue then has labels that never get applied (the print queue’s label exists; the executor doesn’t know about it).
Defensive pattern: validate before dispatch that every label your WMS has printed is included in the envelope. If you have a 1:1 between pre-print and dispatch, instrument a check.
3. Not implementing idempotency on /labels/issue¶
The executor retries on 5xx and timeout. The retries use the same label_print_request_uuid. If your endpoint allocates a new carrier tracking number on each call, retries leak tracking numbers and may double-bill.
Implement idempotency keyed on label_print_request_uuid: return the same label result for retries within a window (default 24h). Even simpler: dedupe on the executor’s label_print_request_uuid in a key-value store; return the cached response on repeat.
4. Treating label_voided_request event as actionable¶
The label_voided_request event_type on confirmation.realtime.v1 is INFORMATIONAL when surfaced as part of cancellation flow. It tells you the executor has already voided its binding atomically with the cancellation. Your action (carrier void) is triggered by the voided_label_bindings[] array in the cancellation ack, not by the realtime event.
The label_voided_request event is OPERATIONAL when surfaced as part of damage reprint flow (Pattern I) — there, it’s followed by a /labels/reprint call. Distinguish by checking voided_reason: order-cancelled is informational; damaged / unreadable / manual-supervisor is operational.
5. Not handling DEFERRED on /labels/bind-pool¶
If an operator scans a pool label when they have no active Task or Order context, your endpoint legitimately should respond DEFERRED. The executor surfaces this back to the operator via Voice/screen: “Label scanned; await next instruction before continuing.” The operator can then scan an Order-source label first, then re-scan the pool label.
Treating DEFERRED as an error breaks this UX. The executor expects DEFERRED as a normal outcome; surface it to your supervisor UI only if it persists.
6. Confusing slot_id with container_id¶
slot_id is fixture-permanent. container_id is movable. In Pattern E /labels/issue calls, both arrive in the context. The label is applied to the container, not the slot. The slot keeps its persistent slot label permanently; the slot label is for slot-identity scanning, not for shipping.
If you write the issued label_id back to the slot in your records, you’ll have stale data the next trip. Write to the container.
Endpoint reference¶
All endpoints sit under your planner’s base URL declared at WES tenant onboarding. Authentication is mTLS using the WES tenant’s cert (same as cancellation flow; see authentication).
POST /wes/v1/labels/issue¶
Direction: Executor → Planner
Purpose: Post-print label issuance at custody event
Idempotency: Keyed on label_print_request_uuid
Request body:
tenant_id: frn
site_id: frn
label_print_request_uuid: uuid
context:
order_id: frn
container_id: frn | null
slot_id: frn | null # populated for Pattern E
fulfilled_contents:
- { sku_id: string, qty_observed: int }
pack_station_id: frn
Response:
issued_labels:
- label_id: string
external_label_uri: string | null
bound_to_type: enum(shipper, container, tote, slot, ...)
bound_to_id: frn
print_payload_uri: string | null
ack_envelope:
outcome: enum(ISSUED, FAILED)
reason: string | null
POST /wes/v1/labels/reprint¶
Direction: Executor → Planner
Purpose: Damage-reprint flow (Pattern I)
Idempotency: Keyed on reprint_uuid
Request body:
tenant_id: frn
site_id: frn
reprint_uuid: uuid
context:
voided_binding_id: frn
voided_label_id: string
voided_reason: enum(damaged, unreadable, manual-supervisor)
preserve_carrier_tracking: bool
Response:
new_binding:
label_id: string
external_label_uri: string | null
bound_to_type: enum
bound_to_id: frn
ack_envelope:
outcome: enum(REPRINTED, FAILED)
reason: string | null
POST /wes/v1/labels/bind-pool¶
Direction: Executor → Planner
Purpose: Pattern F pool-label late-bind
Idempotency: Keyed on scan_uuid
Request body:
tenant_id: frn
site_id: frn
scan_uuid: uuid
context:
label_id: string
scanned_by_agent_id: frn
scan_location: frn
current_task_hint: frn | null
Response:
binding:
label_id: string
bound_to_type: enum
bound_to_id: frn
external_label_uri: string | null
ack_envelope:
outcome: enum(BOUND, DEFERRED, REJECTED)
reason: string | null
Event reference¶
All events flow on the confirmation.realtime.v1 topic with an event_type discriminator. Same delivery semantics as your other realtime events (see Realtime).
|
Direction |
Triggered when |
|---|---|---|
|
Executor → Planner |
Any label scan by operator or AMR. Informational; observe for pick progress. |
|
Executor → Planner |
Container attached to AMR slot or fixed-station slot. |
|
Executor → Planner |
Container removed from slot. |
|
Executor → Planner |
Container arrives at PackStation, RecoveryStation, or dock. |
|
Executor → Planner |
WES needs you to issue a label (followed within ms by |
|
Executor → Planner |
Label voided. |
|
Executor → Planner |
Operator scanned a pool label with no binding (precedes |
All events carry the standard confirmation.realtime.v1 envelope (event_uuid, correlation_id, agent_id, site_id) plus a per-event-type discriminated context field.
FAQ¶
Q: Can I mix print modes within a single Order?
Yes — Pattern C is exactly this. Multi-carton Orders carry N label_bindings each with potentially different print_mode. The most common mix is some cartons pre-printed (high-volume SKUs) and some post-print (custom-pack SKUs).
Q: What if I want to print at a moment that’s neither dispatch nor pack — e.g., at quality-inspection station?
Use post-pick mode with a custom-defined custody point per your Scheme. The executor’s label_print_requested event fires at whatever custody primitive your Scheme declares (PackStation by default for ShipperScheme, but configurable per Scheme via NormativeSpecification).
Q: What if my carrier doesn’t allocate tracking numbers until physical pickup?
Use post-pick mode and have your /labels/issue endpoint synchronously call the carrier just-in-time. Most modern carrier APIs support this within sub-second latency.
Q: Can the executor print my labels itself if I send the print payload at dispatch?
The executor can fetch print-payload URIs (ZPL/EPL/PDF) and route to local Zebra/Honeywell printers via its print-server module. You declare the print payload URI in the dispatch envelope or /labels/issue response. The executor never composes label content from raw parts.
Q: How do I handle multi-jurisdiction relabeling (intl shipment cross-border)? v1 doesn’t support runtime jurisdiction-template switching. If you know the jurisdiction at dispatch, print the correct label up front (Pattern A). If destination changes post-dispatch (rerouting), treat as Pattern I damage-reprint: void original, issue new label with correct template.
Q: What’s the maximum size of a print payload?
The contract doesn’t impose a limit; carrier-issued ZPL is typically <50KB. Treat your print_payload_uri as serving raw bytes; the executor’s printer integration handles routing.
Q: Can I observe label scans from non-binding-aware contexts (e.g., a supervisor walks the floor with a scanner)?
Yes — any scan emits label_scanned. If the scan is by an agent who is not currently claimed to a Task (e.g., a supervisor in browse mode), the event still emits but the executor’s ScanToClaim plan does NOT route to claim. You receive the event purely as observation.
Q: How do I test my integration without a physical AMR/operator?
Use the loopback reference adapter under examples/rest-loopback/ (see Examples). It exercises the full dispatch → print → scan → ack round trip against a stub executor; matches the same OpenAPI schema used in production.
For deeper context, see the Cancellation guide for label-cancellation interaction, and WES ADR-0022 for the architectural decisions behind this contract surface.