Webhooks — Testing¶
Last updated October 20, 2026
There is no separate sandbox environment and no self-service test button. Testing is done against the live API with a real key, plus test events fired by Omnishelf staff — which is why getting your receiver right locally first matters.
Requesting a test event¶
- Register your callback URL with Omnishelf staff and get that webhook's signing secret.
- Ask them to send a test event for the event type you are implementing.
It is delivered to your registered URL with the same headers, the same signature scheme and the same nested shipments → parcels shape as a real event, using representative sample data.
Confirm what you received: a valid signature, an event_id starting evt_, and a data object your parser handles without special-casing.
Verifying signature handling before going live¶
Prove your verification rejects as well as accepts. A verifier that accepts everything passes the happy-path test just as convincingly as a correct one.
# Generate a correct signature locally and replay it at your own endpoint.
BODY='{"event":"fulfilment_order.created","event_id":"evt_test_1","occurred_at":"2026-08-01T10:00:00Z","data":{"order_id":"OMN-KONW-00042","status":"processing"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$OMNISHELF_WEBHOOK_SECRET" -hex | awk '{print $2}')
# 1. Correct signature — expect 200.
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/omnishelf/webhooks \
-H "Content-Type: application/json" \
-H "X-Omnishelf-Event-Id: evt_test_1" \
-H "X-Omnishelf-Event-Type: fulfilment_order.created" \
-H "X-Omnishelf-Signature: sha256=$SIG" \
-H "X-Omnishelf-Timestamp: $(date +%s)" \
-d "$BODY"
# 2. Tampered body, unchanged signature — expect 401.
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/omnishelf/webhooks \
-H "Content-Type: application/json" \
-H "X-Omnishelf-Signature: sha256=$SIG" \
-d '{"event":"fulfilment_order.created","event_id":"evt_test_1","data":{"order_id":"OMN-EVIL-00001"}}'
# 3. Same event twice — expect 200 both times, and exactly one order processed.
Sending a full sample fulfilment_order.created¶
To test order creation end to end, send your endpoint a complete event, the same as a real one. Set three values first:
WEBHOOK_URL— your registered callback URL.OMNISHELF_WEBHOOK_SECRET— thewhsec_…signing secret of the webhook registered for that URL.- In the body, set
warehouse.codeto one of your warehouse codes (seeGET /v1/warehouses) anditems[].skuto a SKU that exists in your system.
WEBHOOK_URL='https://your-system.example.com/omnishelf/webhooks'
OMNISHELF_WEBHOOK_SECRET='whsec_...'
EVENT_ID="evt_test_$(date +%s)"
ORDER_ID="OMN-TEST-$(date +%s)"
# Write the body to a file so the bytes you sign are exactly the bytes you send.
cat > event.json <<EOF
{
"event": "fulfilment_order.created",
"event_id": "$EVENT_ID",
"occurred_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"data": {
"order_id": "$ORDER_ID",
"status": "processing",
"order_date": "$(date -u +%Y-%m-%d)",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"parcel_count": 1,
"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": 1 }
],
"shipments": [
{
"shipment_id": "$ORDER_ID-1",
"sequence": 1,
"carrier": "ups",
"parcels": [
{
"parcel_id": "$ORDER_ID-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" }
}
]
}
]
}
}
EOF
SIG=$(openssl dgst -sha256 -hmac "$OMNISHELF_WEBHOOK_SECRET" -hex < event.json | awk '{print $NF}')
curl -i -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Omnishelf-Event-Id: $EVENT_ID" \
-H "X-Omnishelf-Event-Type: fulfilment_order.created" \
-H "X-Omnishelf-Signature: sha256=$SIG" \
-H "X-Omnishelf-Timestamp: $(date +%s)" \
--data-binary @event.json
Expect a 2xx within 10 seconds. Each run makes a new event_id and order_id, so every run is a new order. To test duplicate handling, run only the curl command again: the event is the same, so no second order should appear.
This is a sample order: it does not exist in Omnishelf, so GET /v1/fulfilment-orders/{order_id} and /labels return 404 for it. To test the label download, ask Omnishelf staff to send a test event for a real order.
A minimal local receiver¶
Run this, expose it with a tunnel (e.g. ngrok http 8000), and give Omnishelf staff the tunnel URL. It logs everything it receives and enforces both verification and idempotency in about thirty lines.
# pip install flask
# export OMNISHELF_WEBHOOK_SECRET=your_secret
# python3 receiver.py
import hmac, hashlib, json, os
from flask import Flask, request
app = Flask(__name__)
# The signing secret of the webhook registered for this URL (each webhook has its own).
SECRET = os.environ["OMNISHELF_WEBHOOK_SECRET"].encode()
SEEN = set() # replace with a real store in production
@app.post("/omnishelf/webhooks")
def receive():
raw = request.get_data()
sent = request.headers.get("X-Omnishelf-Signature", "")
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sent):
print("REJECTED: bad signature")
return "", 401
event_id = request.headers.get("X-Omnishelf-Event-Id")
if event_id in SEEN:
print("DUPLICATE:", event_id)
return "", 200 # already handled — still acknowledge
SEEN.add(event_id)
event = json.loads(raw)
print(event["event"], event["data"]["order_id"], event["data"]["status"])
print(json.dumps(event, indent=2))
return "", 200
if __name__ == "__main__":
app.run(port=8000)
Checklist before going live¶
- [ ] A valid signature is accepted; a tampered body is rejected with 401.
- [ ] A duplicate
event_idis acknowledged with 200 but processed only once. - [ ] Your handler returns within 10 seconds under realistic load.
- [ ] You poll
GET /v1/orders/{order_id}/trackingfor completion rather than waiting for adeliveredevent. - [ ] You handle 429 and 5xx with backoff, and never retry 400 or 404.
- [ ] Your ingestion is idempotent on
order_idso a repeated page from pagination is harmless.