RelayRelayWebhook Events

Webhook events

Relay emits 38 event types. This page is the complete catalog: what each one means, exactly when it fires, and the body it delivers. Register an endpoint with POST /v2/webhooks and name the ones you want in enabled_events — you receive those and nothing else. To receive everything, register enabled_events: ["*"]: the wildcard matches every type on this page and every type added in a later release, so it never needs re-registering as the catalog grows. Signing and verification are covered in Authentication; the wire contract is in the API Reference.

Events: 38 Envelope: id, type, created, data Signature: X-Relay-Signature Audit feed: GET /v2/events

Where to start

Most merchant integrations need about nine of these. The rest exist for warehouse operations, admin auditing, and claims workflows — subscribe to them only if you run those.

EventWhy you want it
shipment.label_readyThe label or QR is fetchable — send it to the shopper.
shipment.in_possessionWe physically have the parcel. This is the event that ends “where is my return?” tickets, and where most merchants start the refund clock.
shipment.injectedHanded to the onward carrier; carrier is populated here.
shipment.deliveredTerminal happy path, with proof of delivery.
shipment.undeliverable
shipment.return_to_sender
Terminal sad path. Both are reachable from one rts request — handle the pair.
shipment.cancelled
shipment.expired
The shopper never sent it. Close the RMA.
shipment.adjustedThe price changed after the quote. Reconcile billing.
shipment.discrepancy_reportedContents did not match the declaration.

The body

Every delivery is the same record, whatever the event. Four blocks: what it is, every id it can be found by, what happened, and — only when the event type has fields of its own — the handful that are specific to it.

{
  "object": "webhook",
  "eventID": "evt_0807c0",
  "event": "scan-event",
  "type": "shipment.delivered",
  "identifiers": {
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 delivered",
    "email": "",
    "full_name": "",
    "status": "delivered",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "delivered",
    "scantime": "2026-08-09T16:04:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "pod": "/v2/documents/doc_pod_7Kd93a.jpg",
    "podGrade": "A",
    "carrier": "Veho"
  }
}
BlockWhat it holds
typeThe exact event type, always — the same string you named in enabled_events. This is what your handler switches on.
eventThe internal LMS name for the event. Usually identical to type, but the ten handling moments all collapse to "scan-event" here. Do not switch on this.
eventIDThe event's id. Stable across retries of the same delivery — use it to make your handler idempotent.
identifiersEvery id this event can be joined on: the parcel, your own order and RMA numbers, the tracking and barcode, the subscription that received it. Keys are absent rather than null when they do not apply.
event_infoWhen it happened (eventDateTime), a one-line summary, the resulting status, and where — location, lat/lon — on the events that happen somewhere.
eventSpecificOnly the fields this event type has. Omitted entirely when there are none, which is the common case — it is never an empty object.
Switch on type, never on event. type is always present and always the exact type you subscribed to. event is our internal name: shipment.injected, shipment.delivered, shipment.in_possession and seven other handling moments all arrive with event: "scan-event", discriminated there only by eventSpecific["scan-type"] — which uses a third spelling again ("possession", not "in_possession").
The full object is not echoed back. The body carries the ids and what happened, not a copy of the shipment. Fetch the resource itself — GET /v2/shipments/{id} — or the event feed at GET /v2/events, keyed by the eventID above, when you need the whole record.
The name prefix is not the subject. shipment.adjusted and shipment.discrepancy_reported both start with shipment. and neither is about a shipment as such — they report an adjustment and a discrepancy. identifiers.shipment is on both, so the parcel is always reachable.

Delivery behavior

The catalog

Creation, label & amendment

Everything that happens before the parcel physically moves. If you are only going to handle a handful of events, shipment.label_ready is the one that tells a shopper their label is fetchable.

shipment.createdShipment

A shipment has been ingested and now exists in Relay. It has an id, a tracking number and a barcode; whether it also has a label yet depends on fulfillment.

Fires whenPOST /v2/shipments, POST /v2/shipments/batch (once per member), and POST /v2/retail/shipments (which also fires shipment.sold).

  • A print_label shipment fires shipment.label_ready in the same call, immediately after this one. A qr_code shipment does not — there is no label until the QR is presented at a counter.
  • carrier is null here in a marketplace-decision market. It is populated at injection, not at creation.
delivered body
{
  "object": "webhook",
  "eventID": "evt_62f467",
  "event": "shipment.created",
  "type": "shipment.created",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "shipment.created",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Shipment created for LMS8418253",
    "status": "created",
    "direction": "return"
  },
  "eventSpecific": {
    "labelVersion": 1,
    "serviceLevel": "ground",
    "parcel": {
      "weight": 2.5,
      "length": 12,
      "width": 9,
      "height": 4,
      "cubicFt": 0.25
    }
  }
}

shipment.label_readyShipment

The label is generated and fetchable from GET /v2/shipments/{id}/label. This is the event to hang "your return label is ready" on.

Fires whenAt creation for fulfillment: "print_label". For fulfillment: "qr_code", when the label is produced on site — the QR is scanned at a PackageHub counter.

  • It fires exactly once per label. A print_label shipment that emitted it at creation does not emit it again when the lifecycle passes through the label_ready status — that suppression is deliberate.
  • A reprint does not re-fire this. Reprints have their own event, shipment.label_reprinted.
delivered body
{
  "object": "webhook",
  "eventID": "evt_677c21",
  "event": "shipment.label_ready",
  "type": "shipment.label_ready",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.label_ready",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Label ready for LMS8418253",
    "status": "label_ready",
    "direction": "return",
    "labelUrl": "/v2/shipments/shp_7Kd93a/label"
  },
  "eventSpecific": {
    "labelFormat": "pdf",
    "labelVersion": 1,
    "serviceLevel": "ground",
    "parcel": {
      "weight": 2.5,
      "length": 12,
      "width": 9,
      "height": 4,
      "cubicFt": 0.25
    }
  }
}

shipment.label_reprintedShipment

A fresh label was minted to replace the current one. The old label is voided.

Fires whenPOST /v2/shipments/{id}/label/reprint

  • label_version increments and supersedes names the label_id just replaced. If you cache label bytes, key the cache on label_id, not on the shipment — otherwise a reprint serves the voided label.
  • The tracking number and barcode do not change on a reprint.
delivered body
{
  "object": "webhook",
  "eventID": "evt_21fd1e",
  "event": "shipment.label_reprinted",
  "type": "shipment.label_reprinted",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_9Xr2Qm",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.label_reprinted",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Label reprinted for LMS8418253",
    "status": "label_ready",
    "direction": "return",
    "labelUrl": "/v2/shipments/shp_7Kd93a/label"
  },
  "eventSpecific": {
    "labelVersion": 2,
    "serviceLevel": "ground",
    "parcel": {
      "weight": 2.5,
      "length": 12,
      "width": 9,
      "height": 4,
      "cubicFt": 0.25
    }
  }
}

shipment.updatedShipment

A pre-drop-off amendment was accepted — parcel dimensions, declared value, destination or drop-off routing.

Fires whenPATCH /v2/shipments/{id}

  • Every amendment re-rates, so amount can move. If the new price or the drop-off routing changes, a new label is minted too — label_version bumps and supersedes is set, exactly as on a reprint.
  • Amendments are refused with 409 shipment_locked once the parcel has been scanned at a PackageHub, so this event never arrives after shipment.ready_for_pickup.
delivered body
{
  "object": "webhook",
  "eventID": "evt_21f373",
  "event": "shipment.updated",
  "type": "shipment.updated",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_9Xr2Qm",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.updated",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 updated",
    "status": "created",
    "direction": "return"
  }
}

shipment.soldShipment

A counter sale completed: a walk-in customer handed a package over, it was priced and paid for, and the label printed on site.

Fires whenPOST /v2/retail/shipments

  • Always direction: "outbound" and fulfillment: "print_label" — the customer is standing at the counter, so there is no QR to present later.
  • This fires alongside shipment.created and shipment.label_ready, not instead of them. Subscribe to shipment.sold only if you need to distinguish counter sales; channel: "retail" already tells you on any of the three.
delivered body
{
  "object": "webhook",
  "eventID": "evt_5ac685",
  "event": "shipment.sold",
  "type": "shipment.sold",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.sold",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 sold at the counter",
    "status": "label_ready",
    "direction": "outbound",
    "labelUrl": "/v2/shipments/shp_7Kd93a/label"
  },
  "eventSpecific": {
    "soldAt": "2026-08-05T14:46:45Z",
    "serviceLevel": "reach_coverage"
  }
}

Drop-off & custody

The parcel is physically moving through our network. In production these are driven by real PackageHub and driver scans. In sandbox you drive them yourself, one rung per call, with POST /v2/test/shipments/{id}/advance.

qr.scannedad-hoc

A customer's QR code was presented and validated at a drop-off counter.

Fires whenPOST /v2/scan-events/walk-in

  • This one is a counter scan, not a parcel milestone. It reports that a QR was presented; identifiers.qr names which, and identifiers.shipment the parcel it belongs to.
  • occurred_at is when the QR was presented at the counter. It is not the same as event_info.eventDateTime, which is when we emitted the event — the two diverge whenever the emit is queued or replayed. Use occurred_at for anything that reasons about custody time.
  • store is the drop-off location resolved from store_id, with its coordinates. It is absent when store_id is not a location we know — never a placeholder, so a missing store reads as unknown rather than as somewhere.
  • It is always immediately followed by shipment.in_possession for the same shipment, and by shipment.adjusted if the counter's reweigh disagreed with the declared parcel.
delivered body
{
  "object": "webhook",
  "eventID": "evt_e9e4ed",
  "event": "scan-event",
  "type": "qr.scanned",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "tracking": "LM7KD93AQ2",
    "store": "loc_a1",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "QR for LM7KD93AQ2 presented at a drop-off location",
    "email": "",
    "full_name": "",
    "lat": "32.784395",
    "lon": "-96.782513",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "location": {
      "name": "PackageHub Deep Ellum",
      "code": "PH-A1",
      "city": "Dallas",
      "state": "TX",
      "postalCode": "75226"
    }
  },
  "eventSpecific": {
    "scan-type": "dropoff",
    "scantime": "2026-08-09T16:04:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "storeCode": "PH-A1",
    "pudoCode": "CPU"
  }
}

shipment.ready_for_pickupShipment

A drop-off location has accepted the package and it is waiting on the shelf for our sweep.

Fires whenPOST /v2/scan-events/accept — the counter's explicit acceptance of a package that arrived with its label already applied.

  • This is the print_label counterpart to shipment.in_possession. A pre-labelled parcel is accepted; a QR parcel is scanned, labelled and taken into custody in one step.
delivered body
{
  "object": "webhook",
  "eventID": "evt_28a2be",
  "event": "scan-event",
  "type": "shipment.ready_for_pickup",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 accepted at the counter",
    "email": "",
    "full_name": "",
    "lat": "32.879232",
    "lon": "-96.960262",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "ready_for_pickup",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "accepted",
    "scantime": "2026-08-09T16:04:00Z",
    "scannerEmail": "",
    "scannerName": ""
  }
}

shipment.picked_upShipment

A driver collected the parcel from the drop-off location on a sweep.

Fires whenA driver collection scan.

delivered body
{
  "object": "webhook",
  "eventID": "evt_6810d4",
  "event": "scan-event",
  "type": "shipment.picked_up",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 collected",
    "email": "",
    "full_name": "",
    "lat": "32.914406",
    "lon": "-96.938682",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "picked_up",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "pickup",
    "scantime": "2026-08-05T21:05:00Z",
    "scannerEmail": "",
    "scannerName": ""
  }
}

shipment.in_possessionShipment

The parcel is in our custody. This is the event that ends "where is my return?" tickets — it is the first hard confirmation the customer actually sent the thing.

Fires whenPOST /v2/scan-events/walk-in (QR presented at a counter), or a custody scan.

  • If you subscribe to only one custody event, make it this one. Most merchants trigger the refund clock here rather than waiting for shipment.delivered.
delivered body
{
  "object": "webhook",
  "eventID": "evt_9f34fb",
  "event": "scan-event",
  "type": "shipment.in_possession",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 taken into LMS possession",
    "email": "",
    "full_name": "",
    "lat": "32.879261",
    "lon": "-96.959788",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "in_possession",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "possession",
    "scantime": "2026-08-05T21:05:00Z",
    "scannerEmail": "",
    "scannerName": ""
  }
}

shipment.at_sort_centerShipment

The parcel arrived at the processing facility.

Fires whenA facility arrival scan.

delivered body
{
  "object": "webhook",
  "eventID": "evt_ab45c5",
  "event": "scan-event",
  "type": "shipment.at_sort_center",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 arrived at the sort centre",
    "email": "",
    "full_name": "",
    "lat": "32.87889",
    "lon": "-96.959918",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "at_sort_center",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "arrived_at_sort",
    "scantime": "2026-08-06T02:40:00Z",
    "scannerEmail": "",
    "scannerName": ""
  }
}

shipment.sortedShipment

The parcel has been processed at the facility and assigned to an onward lane.

Fires whenA sort scan.

delivered body
{
  "object": "webhook",
  "eventID": "evt_d85a53",
  "event": "scan-event",
  "type": "shipment.sorted",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 sorted",
    "email": "",
    "full_name": "",
    "lat": "32.878839",
    "lon": "-96.959807",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "sorted",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "sorted",
    "scantime": "2026-08-06T02:40:00Z",
    "scannerEmail": "",
    "scannerName": ""
  }
}

shipment.injectedShipment

The parcel was handed to the onward carrier. Our leg is done.

Fires whenCarrier injection, or acceptance of an inject tender (POST /v2/tenders/{id}/accept).

  • carrier is populated here in a marketplace-decision market, where it was null from creation onward. If you display a carrier to the customer, this is the earliest event you can reliably read it from.
  • tender_id is set when the injection came through a tender.
delivered body
{
  "object": "webhook",
  "eventID": "evt_8f0ec1",
  "event": "scan-event",
  "type": "shipment.injected",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "tender": "tnd_4Km2Xp",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 handed to the onward carrier",
    "email": "",
    "full_name": "",
    "lat": "32.841026",
    "lon": "-96.996314",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "injected",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "injected",
    "scantime": "2026-08-06T02:40:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "carrier": "Veho"
  }
}

Carrier transit & delivery

The onward carrier has the parcel. These mirror carrier scans, so their cadence is the carrier's, not ours.

shipment.in_transitShipment

The carrier is moving the parcel.

Fires whenThe first carrier movement scan after injection.

  • tracking.updated fires alongside this one, always, and only here. If you subscribe to both you will receive two webhooks carrying the identical Shipment payload.
delivered body
{
  "object": "webhook",
  "eventID": "evt_682596",
  "event": "shipment.in_transit",
  "type": "shipment.in_transit",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "shipment.in_transit",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 in transit",
    "lat": "32.889233",
    "lon": "-97.01168",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "in_transit",
    "direction": "return"
  },
  "eventSpecific": {
    "carrier": "Veho",
    "scheduledDeliveryDate": "2026-08-09"
  }
}

tracking.updatedShipment

Tracking information changed.

Fires whenAlongside shipment.in_transit, and only there.

  • Despite the general-sounding name, this is not a catch-all tracking feed and does not fire on every scan. Today it is emitted at exactly one point in the lifecycle. Poll GET /v2/tracking/{tracking} for the full scan history; do not expect it to arrive by webhook.
delivered body
{
  "object": "webhook",
  "eventID": "evt_aee388",
  "event": "tracking.updated",
  "type": "tracking.updated",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "tracking.updated",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Tracking updated for LMS8418253",
    "lat": "32.912409",
    "lon": "-96.906439",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "in_transit",
    "direction": "return"
  },
  "eventSpecific": {
    "carrier": "Veho",
    "scheduledDeliveryDate": "2026-08-09"
  }
}

shipment.out_for_deliveryShipment

The parcel is on a vehicle for final delivery.

Fires whenAn out-for-delivery carrier scan — or a redeliver disposition, which puts a failed parcel back on this rung.

  • A redeliver disposition deliberately reuses this event rather than minting a new one: the parcel has rejoined the ordinary ladder, so the ladder's own event is the truthful signal. You may therefore see it twice for one shipment.
delivered body
{
  "object": "webhook",
  "eventID": "evt_fb9961",
  "event": "scan-event",
  "type": "shipment.out_for_delivery",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 out for delivery",
    "email": "",
    "full_name": "",
    "lat": "32.826213",
    "lon": "-96.951917",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "out_for_delivery",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "out_for_delivery",
    "scantime": "2026-08-09T09:12:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "carrier": "Veho",
    "scheduledDeliveryDate": "2026-08-09"
  }
}

shipment.deliveredShipment

Delivered. Terminal, and the happy path.

Fires whenA delivery scan.

  • pod and podGrade are populated at delivery and only at delivery. Fetch the image through GET /v2/shipments/{id}/pod.
delivered body
{
  "object": "webhook",
  "eventID": "evt_0807c0",
  "event": "scan-event",
  "type": "shipment.delivered",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 delivered",
    "email": "",
    "full_name": "",
    "lat": "32.878688",
    "lon": "-96.960406",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "delivered",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "delivered",
    "scantime": "2026-08-09T16:04:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "pod": "/v2/documents/doc_pod_7Kd93a.jpg",
    "podGrade": "A",
    "carrier": "Veho"
  }
}

Exceptions & dispositions

The parcel could not complete its journey. Each of these is the resolution of a failed delivery, recorded through POST /v2/shipments/{id}/disposition — except shipment.rejected, which happens at the counter before we ever take custody.

shipment.rejectedShipment

A drop-off location refused the package. We never took custody.

Fires whenPOST /v2/scan-events/reject

  • This payload carries three fields no other Shipment event has: reject_reason, reject_reason_note and rejected_at.
  • The customer is standing at the counter holding the package when this fires. If you notify on it, notify fast.
delivered body
{
  "object": "webhook",
  "eventID": "evt_864fd4",
  "event": "scan-event",
  "type": "shipment.rejected",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "scan-event",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 rejected at the counter",
    "email": "",
    "full_name": "",
    "lat": "32.878682",
    "lon": "-96.959687",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "rejected",
    "direction": "return"
  },
  "eventSpecific": {
    "scan-type": "rejected",
    "scantime": "2026-08-09T16:04:00Z",
    "scannerEmail": "",
    "scannerName": "",
    "rejectReason": "prohibited_item",
    "rejectReasonNote": "Contains a lithium battery pack."
  }
}

shipment.undeliverableShipment

Delivery failed and the parcel is not in our custody — the carrier still has it.

Fires whenPOST /v2/shipments/{id}/disposition with disposition: "rts", when the parcel is past injection.

  • The same rts request produces a different event depending on where the parcel is. Out of our custody it lands here; already in our custody it lands on shipment.return_to_sender. Handle both.
  • This is not terminal. A second rts starts the return leg, and a redeliver puts it back out for delivery.
delivered body
{
  "object": "webhook",
  "eventID": "evt_22f904",
  "event": "shipment.undeliverable",
  "type": "shipment.undeliverable",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.undeliverable",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 could not be delivered",
    "email": "",
    "full_name": "",
    "lat": "32.878854",
    "lon": "-96.960359",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "undeliverable",
    "direction": "return"
  },
  "eventSpecific": {
    "disposition": "rts",
    "carrier": "Veho"
  }
}

shipment.return_to_senderShipment

The parcel is on its way back to the sender. Terminal.

Fires whenPOST /v2/shipments/{id}/disposition with disposition: "rts", when the parcel is already in our custody (status undeliverable or unclaimed).

  • Terminal: no further disposition is accepted, and 409 shipment_not_dispositionable is returned if you try.
delivered body
{
  "object": "webhook",
  "eventID": "evt_a71604",
  "event": "shipment.return_to_sender",
  "type": "shipment.return_to_sender",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.return_to_sender",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 returning to sender",
    "lat": "32.878781",
    "lon": "-96.959584",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "return_to_sender",
    "direction": "return"
  },
  "eventSpecific": {
    "disposition": "rts",
    "carrier": "Veho"
  }
}

shipment.unclaimedShipment

Nobody collected the parcel and the holding clock has started.

Fires whenPOST /v2/shipments/{id}/disposition with disposition: "unclaimed".

  • dispose_after is the date the parcel becomes eligible for disposal — 30 days out. Use it to drive a "last chance" notification.
  • The clock is set by the first report and is not reset by a repeat. A hub re-reporting the same parcel weekly cannot push dispose_after out forever, so a repeat report re-fires this event with the original dates.
delivered body
{
  "object": "webhook",
  "eventID": "evt_ffbf00",
  "event": "shipment.unclaimed",
  "type": "shipment.unclaimed",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.unclaimed",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 unclaimed",
    "lat": "32.878702",
    "lon": "-96.960319",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "unclaimed",
    "direction": "return"
  },
  "eventSpecific": {
    "disposition": "unclaimed"
  }
}

shipment.disposedShipment

The parcel was destroyed or otherwise disposed of. Terminal.

Fires whenPOST /v2/shipments/{id}/disposition with disposition: "dispose".

  • unclaimed_since and dispose_after are deliberately retained on a terminal outcome — they are the record of the aging that justified it.
delivered body
{
  "object": "webhook",
  "eventID": "evt_2c8445",
  "event": "shipment.disposed",
  "type": "shipment.disposed",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.disposed",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 disposed of",
    "lat": "32.878959",
    "lon": "-96.960299",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "disposed",
    "direction": "return"
  },
  "eventSpecific": {
    "disposition": "dispose"
  }
}

claim.openedClaim

A claim was opened against a shipment — lost, damaged or misdelivered.

Fires whenPOST /v2/claims

  • amount_approved is null at opening. There is currently no event for a claim being approved, denied or closed — poll GET /v2/claims/{id}.
delivered body
{
  "object": "webhook",
  "eventID": "evt_065c97",
  "event": "claim.opened",
  "type": "claim.opened",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "claim": "clm_5Rt8Wq",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "claim.opened",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Claim clm_5Rt8Wq opened against shp_7Kd93a",
    "status": "open"
  },
  "eventSpecific": {
    "claimType": "lost",
    "description": "Parcel not scanned since the sort centre; customer refunded."
  }
}

Cancellation, hold & expiry

The shipment stopped moving, by request or by the clock.

shipment.cancelledShipment

The shipment was cancelled and its label voided.

Fires whenPOST /v2/shipments/{id}/cancel

  • label_voided flips to true. A voided label must not be presented at a counter; if you have already emailed it, this is the event to send the retraction on.
delivered body
{
  "object": "webhook",
  "eventID": "evt_e235ec",
  "event": "shipment.cancelled",
  "type": "shipment.cancelled",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.cancelled",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 cancelled",
    "status": "cancelled",
    "direction": "return"
  },
  "eventSpecific": {
    "labelVoided": true,
    "voidedAt": "2026-08-05T16:30:00Z",
    "cancelledAt": "2026-08-05T16:30:00Z",
    "expiresAt": "2026-08-08T14:46:45Z"
  }
}

shipment.expiredShipment

The customer never sent the parcel and the window closed.

Fires whenLazily — when the shipment is next read after expires_at has passed, and once only.

  • This event is not emitted by a clock. Expiry is evaluated on read, so the event fires the first time anything looks at the shipment after expires_at — which may be well after that timestamp, or not at all until something asks. Do not treat its arrival time as the expiry time; read expired_at.
  • It fires exactly once. A second read of an already-expired shipment does not re-fire it.
delivered body
{
  "object": "webhook",
  "eventID": "evt_205357",
  "event": "shipment.expired",
  "type": "shipment.expired",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m",
    "qr": "qr_htBmbq"
  },
  "event_info": {
    "event": "shipment.expired",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 expired before it was ever possessed",
    "status": "expired",
    "direction": "return"
  },
  "eventSpecific": {
    "labelVoided": true,
    "voidedAt": "2026-08-08T14:46:45Z",
    "expiredAt": "2026-08-08T14:46:45Z",
    "expiresAt": "2026-08-08T14:46:45Z"
  }
}

shipment.heldShipment

The shipment was placed on hold and will not advance until released.

Fires whenPOST /v2/shipments/{id}/hold

  • status does not change — the shipment keeps whatever rung it was on. The hold lives in the boolean held field, so a handler that switches only on status will miss it entirely.
delivered body
{
  "object": "webhook",
  "eventID": "evt_fa9f96",
  "event": "shipment.held",
  "type": "shipment.held",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.held",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 held",
    "status": "created",
    "direction": "return"
  },
  "eventSpecific": {
    "held": true
  }
}

shipment.releasedShipment

A hold was lifted and the shipment may advance again.

Fires whenPOST /v2/shipments/{id}/release

delivered body
{
  "object": "webhook",
  "eventID": "evt_b3fa5e",
  "event": "shipment.released",
  "type": "shipment.released",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.released",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 released from hold",
    "status": "created",
    "direction": "return"
  },
  "eventSpecific": {
    "held": false
  }
}

shipment.redirectedShipment

The destination address was changed in flight.

Fires whenPOST /v2/shipments/{id}/redirect

  • Read the new destination from to. The tracking number does not change.
delivered body
{
  "object": "webhook",
  "eventID": "evt_0fb955",
  "event": "shipment.redirected",
  "type": "shipment.redirected",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "barcode": "LMS8418253",
    "tracking": "LM7KD93AQ2",
    "rma": "RMA-4471",
    "orderID": "ORD-88123",
    "shipExtID": "OC-ORDER-55219",
    "label": "lbl_pfiCYF",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.redirected",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "LMS8418253 redirected",
    "status": "in_transit",
    "direction": "return",
    "location": {
      "name": "Dana Reyes",
      "city": "Dallas",
      "state": "TX",
      "postalCode": "75202",
      "country": "US"
    }
  },
  "eventSpecific": {
    "redirectedTo": "Dana Reyes",
    "redirectedToLine1": "77 Elm St",
    "carrier": "Veho"
  }
}

Price adjustments & discrepancies

Anything that changes what the shipment costs after it was quoted. If you reconcile invoices, these are the events that matter most.

shipment.adjustedAdjustment

The price changed after the fact, because the parcel was not what was declared.

Fires whenThree different places: a reweigh/re-dim at the counter (POST /v2/scan-events/walk-in), an explicit POST /v2/shipments/{id}/adjustments, and a chargeable discrepancy (POST /v2/discrepancies).

  • The payload is an Adjustment, not a Shipment, even though the event name begins with shipment.. Read shipment_id to find the parcel.
  • delta_amount is signed — a re-weigh can go down. All money is in minor units (cents).
  • A discrepancy that is chargeable fires both this and shipment.discrepancy_reported, and the discrepancy payload's adjustment_id points at this adjustment.
delivered body
{
  "object": "webhook",
  "eventID": "evt_b74d79",
  "event": "shipment.adjusted",
  "type": "shipment.adjusted",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "adjustment": "adj_3Np7Vk",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.adjusted",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "shp_7Kd93a repriced by adjustment adj_3Np7Vk"
  },
  "eventSpecific": {
    "reason": "reweigh",
    "originalWeight": 2.5,
    "newWeight": 4.0
  }
}

shipment.discrepancy_reportedDiscrepancy

Declared parcel details did not match what was observed, recorded up front as a formal discrepancy.

Fires whenPOST /v2/discrepancies

  • stage is dropoff or sort — where the mismatch was caught.
  • chargeable: false means it was recorded but not billed, and adjustment_id is then null.
delivered body
{
  "object": "webhook",
  "eventID": "evt_a82bd7",
  "event": "shipment.discrepancy_reported",
  "type": "shipment.discrepancy_reported",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "discrepancy": "dsc_8Qm4Zt",
    "adjustment": "adj_3Np7Vk",
    "store": "ph_store_4821",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "shipment.discrepancy_reported",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Discrepancy dsc_8Qm4Zt reported against shp_7Kd93a",
    "lat": "32.878735",
    "lon": "-96.959772",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z"
  },
  "eventSpecific": {
    "stage": "dropoff",
    "declared": {
      "weight": 2.5,
      "length": 12,
      "width": 9,
      "height": 4
    },
    "observed": {
      "weight": 4.0,
      "length": 14,
      "width": 10,
      "height": 5
    },
    "chargeable": true
  }
}

adjustment.disputedAdjustment

A partner formally disputed a price adjustment.

Fires whenPOST /v2/adjustments/{adj_id}/dispute

  • Same Adjustment shape as shipment.adjusted, with dispute_status and dispute_reason now populated.
  • There is no event for a dispute being resolved. Poll GET /v2/shipments/{id}/adjustments.
delivered body
{
  "object": "webhook",
  "eventID": "evt_8a014b",
  "event": "adjustment.disputed",
  "type": "adjustment.disputed",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "adjustment": "adj_3Np7Vk",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "adjustment.disputed",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Adjustment adj_3Np7Vk disputed for shp_7Kd93a"
  },
  "eventSpecific": {
    "reason": "reweigh",
    "originalWeight": 2.5,
    "newWeight": 4.0,
    "disputeStatus": "open",
    "disputeReason": "weight_incorrect"
  }
}

QR lifecycle

Only relevant if you use fulfillment: "qr_code". Two of these three carry an ad-hoc payload rather than a documented object.

qr.reissuedad-hoc

A replacement QR was issued and the previous one invalidated.

Fires whenPOST /v2/shipments/{id}/qr/reissue

  • No object field — this is a bare three-key record, like qr.scanned.
  • The old QR stops validating the moment this fires. If the customer has the previous email open, that code is already dead.
delivered body
{
  "object": "webhook",
  "eventID": "evt_ea272a",
  "event": "qr.reissued",
  "type": "qr.reissued",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "qr": "qr_Pw3Nc9",
    "previousQr": "qr_htBmbq",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "qr.reissued",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "QR reissued for shp_7Kd93a"
  }
}

qr.invalidatedQR

A QR was invalidated and can no longer be presented.

Fires whenPOST /v2/qr/{qr_id}/invalidate

  • A reissue invalidates the old QR too, but reports it as qr.reissued rather than firing this event for the superseded code.
delivered body
{
  "object": "webhook",
  "eventID": "evt_30fbc9",
  "event": "qr.invalidated",
  "type": "qr.invalidated",
  "identifiers": {
    "ltracking": "LR4T8XKP2M",
    "shipment": "shp_7Kd93a",
    "qr": "qr_htBmbq",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "qr.invalidated",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "QR invalidated for shp_7Kd93a",
    "status": "invalidated"
  },
  "eventSpecific": {
    "qrStatus": "invalidated",
    "expiresAt": "2026-08-08T14:46:45Z"
  }
}

Pickups, tenders & manifests

Operational events between us and a carrier or a store. Useful to a warehouse or ops dashboard; usually noise to a merchant integration.

pickup.scheduledPickup

A pickup was booked for a shipment.

Fires whenPOST /v2/pickups

delivered body
{
  "object": "webhook",
  "eventID": "evt_7adb39",
  "event": "pickup.scheduled",
  "type": "pickup.scheduled",
  "identifiers": {
    "ltracking": "LR9BC5HTQ7",
    "shipment": "shp_7Kd93a",
    "pickup": "pkp_2Fh6Ly",
    "confirmation": "PK00UM54LNT0",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "pickup.scheduled",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Pickup pkp_2Fh6Ly scheduled for shp_7Kd93a",
    "lat": "32.879237",
    "lon": "-96.959706",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "scheduled"
  },
  "eventSpecific": {
    "pickupDate": "2026-08-10",
    "pickupWindowStart": "09:00",
    "pickupWindowEnd": "17:00",
    "packageCount": 1
  }
}

pickup.cancelledPickup

A booked pickup was cancelled.

Fires whenPOST /v2/pickups/{id}/cancel, and PATCH /v2/pickups/{id} setting status to cancelled.

delivered body
{
  "object": "webhook",
  "eventID": "evt_a4b692",
  "event": "pickup.cancelled",
  "type": "pickup.cancelled",
  "identifiers": {
    "ltracking": "LR9BC5HTQ7",
    "shipment": "shp_7Kd93a",
    "pickup": "pkp_2Fh6Ly",
    "confirmation": "PK00UM54LNT0",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "pickup.cancelled",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Pickup pkp_2Fh6Ly cancelled for shp_7Kd93a",
    "lat": "32.879315",
    "lon": "-96.959553",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "cancelled"
  },
  "eventSpecific": {
    "pickupDate": "2026-08-10",
    "pickupWindowStart": "09:00",
    "pickupWindowEnd": "17:00",
    "packageCount": 1
  }
}

sweep.createdSweep

A collection visit was registered for a store and a day.

Fires whena sweep is ordered up — one carrier visit to one store, whatever it ends up collecting.

  • A sweep is one visit, not one parcel. Every shipment collected on it carries the same sweepId, and the carrier is paid once whether the driver takes one box or ten.
  • There is no shipment identifier and no package count, because at this moment there are no parcels on it yet. Parcels join a live sweep as they are handed over. Ask GET /v2/shipments?sweepId=SW7K3D9A for the membership as it stands now.
  • status is ordered at creation. A sweep stays open while any live pickup remains on it; it closes only when every pickup on it has been cancelled.
  • The store block is copied onto the sweep, not looked up. A sweep is billing evidence, so what the store was called and where it stood on the day survives that store later being renamed, moved or closed.
delivered body
{
  "object": "webhook",
  "eventID": "evt_35c601",
  "event": "sweep.created",
  "type": "sweep.created",
  "identifiers": {
    "sweep": "SW7K3D9A",
    "store": "PH4821",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "sweep.created",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Sweep SW7K3D9A created",
    "lat": "33.965048",
    "lon": "-118.151936",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "ordered",
    "location": {
      "name": "PackageHub Bell Gardens",
      "code": "PH4821",
      "city": "Bell Gardens",
      "state": "CA",
      "postalCode": "90201",
      "country": "US"
    }
  },
  "eventSpecific": {
    "sweepDate": "2026-08-30",
    "dp": "HOV",
    "storeCode": "PH4821",
    "storeAddress": "6621 Eastern Ave, Bell Gardens, CA 90201"
  }
}

tender.createdTender

A batch of shipments was assembled for handoff to a carrier.

Fires whenPOST /v2/tenders

  • piece_count is derived from the number of member shipments, not taken from your request. A piece_count you send is ignored.
  • shipments[].status is recomputed live on every read, so it reflects each member's current state rather than a snapshot taken at tender time.
delivered body
{
  "object": "webhook",
  "eventID": "evt_ca6c11",
  "event": "tender.created",
  "type": "tender.created",
  "identifiers": {
    "tender": "tnd_4Km2Xp",
    "facility": "lms_sort_dfw",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "tender.created",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Tender tnd_4Km2Xp created",
    "lat": "32.878721",
    "lon": "-96.960023",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "created"
  },
  "eventSpecific": {
    "carrier": "GoFo",
    "handoff": "inject",
    "pieceCount": 2,
    "shipmentCount": 2
  }
}

tender.acceptedTender

The carrier signed for the batch.

Fires whenPOST /v2/tenders/{id}/accept

  • Every member shipment advances at the same moment — to injected for an inject tender, made_ready for a collect one — so you will also receive a shipment.injected per member.
  • This fires before the piece count is checked, so it arrives even when the count is short. See tender.discrepancy.
delivered body
{
  "object": "webhook",
  "eventID": "evt_cfa373",
  "event": "tender.accepted",
  "type": "tender.accepted",
  "identifiers": {
    "tender": "tnd_4Km2Xp",
    "facility": "lms_sort_dfw",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "tender.accepted",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Tender tnd_4Km2Xp accepted",
    "lat": "32.879026",
    "lon": "-96.960193",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "accepted"
  },
  "eventSpecific": {
    "carrier": "GoFo",
    "handoff": "inject",
    "pieceCount": 2,
    "pieceCountReceived": 2,
    "shipmentCount": 2,
    "acceptedBy": "R. Alvarez",
    "acceptedAt": "2026-08-06T14:20:00Z"
  }
}

tender.discrepancyTender

The carrier received a different number of pieces than we tendered.

Fires whenPOST /v2/tenders/{id}/accept, when piece_count_received differs from the tender's derived piece_count.

  • It always arrives second, after tender.accepted for the same tender — acceptance is recorded first, then the count is reconciled. A handler that treats tender.accepted as "all pieces confirmed" is wrong.
  • discrepancy.type is short or over; delta is signed. shipment_ids lists the whole tender, not just the missing pieces — which ones are missing is not known at this point.
delivered body
{
  "object": "webhook",
  "eventID": "evt_972bcb",
  "event": "tender.discrepancy",
  "type": "tender.discrepancy",
  "identifiers": {
    "tender": "tnd_4Km2Xp",
    "facility": "lms_sort_dfw",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "tender.discrepancy",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Tender tnd_4Km2Xp accepted short",
    "lat": "32.878917",
    "lon": "-96.960446",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "accepted"
  },
  "eventSpecific": {
    "carrier": "GoFo",
    "handoff": "inject",
    "pieceCount": 2,
    "pieceCountReceived": 1,
    "shipmentCount": 2,
    "acceptedBy": "R. Alvarez",
    "acceptedAt": "2026-08-06T14:22:00Z",
    "discrepancyType": "short",
    "expectedPieceCount": 2,
    "receivedPieceCount": 1,
    "delta": -1
  }
}

manifest.closedManifest

A store's daily manifest was finalized.

Fires whenPOST /v2/manifests/{id}/close

  • The identifier field is manifest_id, not id. This is the only event payload in the catalog that does not carry an id.
delivered body
{
  "object": "webhook",
  "eventID": "evt_82c4d2",
  "event": "manifest.closed",
  "type": "manifest.closed",
  "identifiers": {
    "manifest": "mfst_6Yn1Bd",
    "store": "ph_store_4821",
    "customer": "Acme Returns",
    "partner": "acme_returns",
    "webhook": "whk_A7fQ2m"
  },
  "event_info": {
    "event": "manifest.closed",
    "date": "2026-08-09",
    "eventDateTime": "2026-08-09T16:04:00Z",
    "summary": "Manifest mfst_6Yn1Bd closed",
    "lat": "32.878879",
    "lon": "-96.960447",
    "coordinateSource": "device",
    "scannedDateTime": "2026-08-09T16:03:52Z",
    "status": "closed"
  },
  "eventSpecific": {
    "packageCount": 2,
    "manifestDate": "2026-08-06",
    "closedAt": "2026-08-06T23:59:00Z"
  }
}