Webhooks — Payload Shape & Headers¶
Last updated October 19, 2026
Payload shape¶
Every event is the same four-field envelope. The data object is byte-for-byte the same shape as the object returned by GET /v1/fulfilment-orders/{order_id} — it is built by the same code path and passed through the same serializer, so the two cannot drift apart. Write one parser and use it for both.
{
"event": "fulfilment_order.created",
"event_id": "evt_9f2c1b4e-6d31-4f7a-9c0e-8b2a5d41e770",
"occurred_at": "2026-07-29T09:10:04Z",
"data": {
"order_id": "OMN-KONW-00042",
"status": "processing",
"order_date": "2026-07-28",
"created_at": "2026-07-28T14:22:00Z",
"updated_at": "2026-07-29T09:10:00Z",
"parcel_count": 3,
"warehouse": { "code": "TETRIS", "name": "Tetris Prep" },
"shipping_address": {
"name": "Jordan Ellis",
"line1": "1420 Sycamore Ave",
"line2": null,
"city": "Baltimore",
"state": "MD",
"postal_code": "21201",
"country": "US"
},
"items": [
{ "sku": "YT-FL-RV-A-028-YE-1", "product_name": "31000 Lumen LED Work Lights", "quantity": 5 }
],
"shipments": [
{
"shipment_id": "OMN-KONW-00042-1",
"sequence": 1,
"carrier": "ups",
"parcels": [
{
"parcel_id": "OMN-KONW-00042-1-1",
"parcel_number": 1,
"tracking_number": "1Z0K49Y40312446341",
"carrier": "ups",
"has_label": true,
"weight": 11.0,
"weight_unit": "lb",
"dimensions": { "length": 17.0, "width": 13.0, "height": 5.0, "unit": "in" }
}
]
}
]
}
}
| Field | Type | Notes |
|---|---|---|
event |
String | One of the event types in the catalogue. |
event_id |
String | Unique per event, prefixed evt_. Use it as your idempotency key. |
occurred_at |
Timestamp | ISO 8601 UTC with a Z suffix. |
data |
Object | The full fulfilment order. Same shape as the GET response. |
On fulfilment_order.cancelled and fulfilment_order.on_hold, data carries one extra field, previous_status, holding the status the order left:
{
"event": "fulfilment_order.cancelled",
"event_id": "evt_31c8a0d5-77bd-4a2e-8f19-1cf3f0b6a942",
"occurred_at": "2026-07-30T16:48:11Z",
"data": {
"order_id": "OMN-KONW-00042",
"status": "cancelled",
"previous_status": "processing",
...
}
}
Delivery headers¶
Every delivery carries Content-Type: application/json plus four Omnishelf headers:
| Header | Example | What it is for |
|---|---|---|
X-Omnishelf-Event-Id |
evt_9f2c1b4e-... |
Same value as event_id in the body. Deduplicate on this without parsing the payload. |
X-Omnishelf-Event-Type |
fulfilment_order.created |
Route to the right handler before parsing. |
X-Omnishelf-Signature |
sha256=8f14e45f... |
HMAC-SHA256 of the raw body, hex-encoded, with a sha256= prefix. |
X-Omnishelf-Timestamp |
1785488503 |
Unix epoch seconds at send time. Useful for rejecting very stale deliveries. |
POST /omnishelf/webhooks HTTP/1.1
Host: api.your-company.com
Content-Type: application/json
X-Omnishelf-Event-Id: evt_9f2c1b4e-6d31-4f7a-9c0e-8b2a5d41e770
X-Omnishelf-Event-Type: fulfilment_order.created
X-Omnishelf-Signature: sha256=8f14e45fceea167a5a36dedd4bea2543a1b4e2c9d0f7361b8e4c2a9d13f5e074
X-Omnishelf-Timestamp: 1785488503
The signature is not timestamped into the signed string: it is a plain HMAC over the body alone. The timestamp header is informational, so use it for staleness checks but do not include it in the HMAC input.