Skip to content

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

  1. Register your callback URL with Omnishelf staff and get that webhook's signing secret.
  2. 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 — the whsec_… signing secret of the webhook registered for that URL.
  • In the body, set warehouse.code to one of your warehouse codes (see GET /v1/warehouses) and items[].sku to 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_id is acknowledged with 200 but processed only once.
  • [ ] Your handler returns within 10 seconds under realistic load.
  • [ ] You poll GET /v1/orders/{order_id}/tracking for completion rather than waiting for a delivered event.
  • [ ] You handle 429 and 5xx with backoff, and never retry 400 or 404.
  • [ ] Your ingestion is idempotent on order_id so a repeated page from pagination is harmless.