WEBHOOKS

Webhooks

Receive real-time events from Superroute — orders, status changes, tracking updates. With signed payloads, automatic retries, and a built-in debugger.

Webhook Integration Guide
Developer Center Home

Webhook Integration Guide

What are webhooks?

A webhook is an HTTP POST that Superroute sends to a URL you configure whenever something happens — an order is created, a delivery is completed, a tracking event is recorded. You build a receiver endpoint, we deliver the event there.

How delivery works

Events are queued and sent asynchronously. Each request carries an HMAC-SHA256 signature so you can verify it really came from us. Failed deliveries (non-2xx response or timeout) are retried with exponential backoff up to 5 times.

Security model

You configure a shared secret on the settings page. Every outgoing webhook is signed with that secret. Your receiver recomputes the signature and compares — if they match, the payload is genuine and untampered.

Event Catalog

Eight outbound event types are available. Each has its own URL field on your settings page — you can subscribe to any subset.

Machine-readable description of every outbound event, with payload schemas, headers and retry schedules (AsyncAPI 3.0): asyncapi-webhooks.json

order.created

Fires whenever a local-delivery order (Delivery / Pickup / P2P) is created via any path — web form, REST/GraphQL API, ecommerce platform sync, automatic rules, import rows, etc. Excludes label-service and other non-delivery order types. Skipped in batch flow when order_create_async_postback_url is also configured for the same recipient. Configure with order_create_webhook_url.

Payload Examples
order.status_change

Fires on every order status transition — picked up, in transit, delivered, exception, cancelled. Configure with order_status_change_webhook_url.

Payload Examples
tracking.event

Fires on each tracking lifecycle event recorded against a package (information submitted, start delivery, delivery success, not delivered, etc.). Configure with tracking_event_webhook_url. Delivered and picked-up events also carry the proof of delivery: proof_files plus proof_files_detail (file_id, type, url, full_url, signed download URL). Photos uploaded after the event arrive as pod.files_updated. Each file also carries its event context: tracking_event_id, tracking_event_status_id, tracking_event_key, service_type (1 = delivery / 2 = pickup) and service_status (1 = success / 2 = failed), all null for legacy files without a recorded event.

Payload Examples
order.create_async

Fires once after a batch order import finishes processing. Payload contains the per-row results array. Configure with order_create_async_postback_url.

Payload Examples
POD Files Updated

Fired when a delivery photo or signature is added, replaced or removed (action: added / updated / removed) — one delivery per file, no more polling for attachments. Opt-in via pod_files_webhook_url. Each file also carries its event context: tracking_event_id, tracking_event_status_id, tracking_event_key, service_type (1 = delivery / 2 = pickup) and service_status (1 = success / 2 = failed), all null for legacy files without a recorded event. When staff replace a photo or signature, or set an archived version as current, the file is sent with action: updated and carries only the current image; archived versions are never sent.

Payload Examples
Order Deleted

Fired when an order is permanently deleted, so your system can mirror the removal. Opt-in via order_deleted_webhook_url.

Payload Examples
Order Cancel Failed

Fired when a cancel attempt is rejected (for example the order is already out for delivery), so your ops pipeline can monitor failed cancels without polling API responses. Opt-in via order_cancel_failed_webhook_url.

Payload Examples
Route Board Seat Changed

Fired whenever a Route Board seat changes hands or the board changes state — action tells you what happened (claimed, standby, pooled, promoted, withdrawn, vetoed, replaced, assigned, awarded, lost, displaced, settled, board_opened, board_closed, board_cancelled). Business-level only. Opt-in via route_board_webhook_url.

Payload Examples

Provider Locker Events

A separate webhook channel for third-party delivery providers integrating with smart lockers. Events are delivered to the endpoint configured for your provider account, and each endpoint can subscribe to any subset of event types.

partner_locker.delivery.doors_opened

Doors Opened — Fires the moment compartment doors open for a delivery attempt — whether the courier used the locker screen with the access code or the remote-open API — including reassignment re-openings. The opening block lists every opened compartment with its grid_id, hardware compartment_number and pickup_locker_number (sequential display number counted top-to-bottom within a column, then left to right). Each compartment also carries pickup_locker_code — the "{shelf_code}-{pickup_locker_number}" label the recipient is sent to, null when the compartment has no pickup number. The payload also includes pickup_code — the recipient pickup code, allocated the moment the doors open; it remains the same code after the courier confirms the deposit, and only becomes usable for pickup once the deposit is confirmed.

Payload Examples
partner_locker.delivery.delivered

Delivered to Locker — Fires when a drop-off is confirmed and the parcels are in the locker. The payload includes the recipient pickup code. Each compartment also carries pickup_locker_code — the "{shelf_code}-{pickup_locker_number}" label the recipient is sent to, null when the compartment has no pickup number. For doors opened through the remote-open API the deposit is settled by the platform as soon as the cabinet reports every opened door closed, so this event fires without a confirm call; confirmed_by names the path that settled: courier_terminal, partner_api, door_close, timeout_door_closed or console.

Payload Examples
partner_locker.pickup.completed

Picked Up — Fires when the recipient has collected the deposited packages.

Payload Examples
partner_locker.delivery.failed

Delivery Failed — Fires when a delivery fails; per-package failure codes are included.

Payload Examples
partner_locker.delivery.expired

Expired — Fires when an unused delivery code or an uncollected deposit passes its expiry time.

Payload Examples
partner_locker.delivery.cancelled

Cancelled — Fires when a delivery is cancelled before completion.

Payload Examples
partner_locker.delivery.correction_reopened

Correction Reopened — Fires when the deposited compartments are reopened within the correction window to fix a wrong placement — from the locker screen or via the API. The correction block lists the reopened compartments. Each compartment also carries pickup_locker_code — the "{shelf_code}-{pickup_locker_number}" label the recipient is sent to, null when the compartment has no pickup number.

Payload Examples
partner_locker.delivery.code_rotated

Pickup Code Changed — Fires when the partner rotates a delivery's access code or pickup code. The rotation block names which code was replaced, when, and whether the recipient notification was re-sent — the new code itself never travels in a webhook; it is revealed only in the direct rotation API response.

Payload Examples
carrier_package.distribution.created

Carrier Package Distributed — Fires when a pre-advised carrier package received at one warehouse is placed on a distribution order to the location the carrier named. The data block carries the package number, carrier reference and member number, the distribution order (order_id, order_ref, status) and the origin and destination locations. Recorded only for carriers with a provider account.

Payload Examples
carrier_package.distribution.loaded

Carrier Package Loaded — Fires when the package is scanned onto the truck at the origin warehouse; distribution.status is in_transit and loaded_at is set.

Payload Examples
carrier_package.distribution.delivered

Carrier Package Delivered to Location — Fires when the driver hands the distribution order over at the location; distribution.status is delivered and delivered_at is set. Shelving is a later, separate event.

Payload Examples
carrier_package.distribution.shelved

Carrier Package Shelved at Location — Fires when the package is shelved at the location, into a locker compartment or onto a shelf; location carries grid_id, grid_code and shelf_code. The recipient's pickup notification goes out at this moment.

Payload Examples
carrier_package.distribution.removed

Carrier Package Removed from Distribution — Fires when the package is taken off a distribution order before it left, or the order was cancelled; distribution.reason is removed or cancelled. The package is back on the warehouse's distribution list.

Payload Examples

Signature & Verification: Provider locker webhooks use their own signing scheme: X-Webhook-Signature is base64(HMAC-SHA256(secret, timestamp + "\n" + delivery id + "\n" + raw body)), where the timestamp and delivery id come from the X-Webhook-Timestamp and X-Webhook-Delivery-Id headers. Also verify X-Webhook-Content-Digest (SHA-256 of the body) and reject stale timestamps. X-Webhook-Id stays stable across retries — use it for idempotency.

How to Configure: Endpoints are managed under Third-Party Delivery → Provider Locker → Settings, one endpoint per provider, with a selectable event list. Failed deliveries are retried with exponential backoff for up to 7 attempts before dead-lettering; dead-lettered events can be retried manually from the Events page.

Sandbox (mock cabinet) events: Deliveries created against mock cabinets emit the same webhook events as production, signed with the same secret, so you can develop against realistic traffic. Sandbox events are marked in three ways: the payload carries "livemode": false, the event_id starts with PLE-MOCK-, and the request includes the X-Webhook-Test: 1 header. If a sandbox URL is configured on the endpoint, sandbox events go there instead of the production URL; otherwise they fall back to the production URL, still marked. The "Deliver sandbox events" switch stops sandbox delivery entirely.

Third-Party Delivery Events

Package-delivery webhooks pushed to third-party delivery providers (couriers). They cover the delivery assignment lifecycle so a courier no longer needs to poll for new jobs. This category is separate from the Smart Locker events below: each provider configures an independent endpoint, signing secret and event subscription per category — in its own provider portal or by the platform operator.

delivery.assignment.created

Assignment Created — Fires when an order is assigned to the provider — by an automatic rule or manually. The payload carries the assignment number, order identifiers and package tracking numbers.

Payload Examples
delivery.assignment.handed_over

Packages Handed Over — Fires when the warehouse has physically handed all packages of the assignment over to the provider.

Payload Examples
delivery.assignment.cancelled

Assignment Cancelled — Fires when the platform withdraws an assignment from the provider. The reason field distinguishes cancelled (the assignment was cancelled at the carrier), fallback_to_self_delivery (the platform took the order back into self delivery) and reassigned (the order was moved to another provider).

Payload Examples
delivery.assignment.partial_delivered

Partially Delivered — Fires when part of a shipment has been delivered while other packages are still in progress. The packages array carries each parcel's outcome, and legs lists the external orders booked with the carrier — one per package when the carrier does not accept multi-piece shipments.

Payload Examples

Signature & Verification: Third-party delivery webhooks use the same signing scheme as provider locker webhooks: X-Webhook-Signature is base64(HMAC-SHA256(secret, timestamp + "\n" + delivery id + "\n" + raw body)), with the timestamp and delivery id taken from the X-Webhook-Timestamp and X-Webhook-Delivery-Id headers. Also verify X-Webhook-Content-Digest (SHA-256 of the body) and reject stale timestamps. X-Webhook-Id stays stable across retries — use it for idempotency.

How to Configure: Providers configure this endpoint themselves in the provider portal (Webhook Settings), or the platform operator does so under Third-Party Delivery → Providers → Webhooks. One endpoint per provider with a selectable event list. The signing secret can be auto-generated or set to a custom value, and is viewable on the settings page. Failed deliveries are retried with exponential backoff for up to 7 attempts before dead-lettering; dead-lettered events can be retried manually. A signed test (mock) event can be sent from the settings page at any time — test requests carry the X-Webhook-Test: 1 header and "test": true inside the payload data.

OHP Push (Order Handoff Protocol)

Signed pushes about handoffs exchanged over the Open Order Handoff Protocol: lifecycle changes (accepted, rejected, expired, cancelled), amendment answers, tracking events and new settlement lines. Subscribed per OHP token via POST /api/v1/ohp/subscriptions; the endpoint must echo a challenge before the subscription exists. Every payload is the OHP envelope; message_id stays the same on every retry — deduplicate on it. Pull remains the source of truth.

handoff.accepted handoff.rejected handoff.expired handoff.cancelled amendment.accepted amendment.rejected handoff.tracking settlement_line.created
Payload Examples

Signature & Verification: X-Ohp-Signature: v1= + hex HMAC-SHA256 of "{timestamp}.{raw body}" with the subscription secret. X-Ohp-Timestamp changes per attempt; X-Ohp-Delivery equals message_id. CloudEvents and Standard Webhooks are available per subscription.

How to Configure: Managed with the OHP token: POST /api/v1/ohp/subscriptions (url, secret, events, format, signature), GET to list, DELETE to revoke. The machine-readable catalog lists every event under the ohp sender.

Order Lifecycle Events

Fine-grained, opt-in events next to the classic order.status_change webhook (which is unchanged): who was assigned, whether the driver accepted, when the parcel was picked up, on the way, delivered or failed, plus driver duty changes and throttled driver positions. Nothing is sent until you configure the URLs below.

order.assigned

A driver was assigned to the order (manually, by route planning or by automatic assignment). data.source = auto_assign when the orchestrator did it.

Payload Examples
order.unassigned

The order lost its driver (hand-off, revoke, rejection, time-out). data.previous_driver_id tells who had it.

Payload Examples
order.accepted

The driver accepted an auto-assigned order in the app (driver duty with acceptance required).

Payload Examples
order.rejected

The driver rejected an assigned order; data.reason carries the optional free-text reason.

Payload Examples
order.pickup_started

The driver started the pickup (status Start Pickup / Out for Pickup).

Payload Examples
order.picked_up

The parcel was picked up (status Already Picked Up).

Payload Examples
order.on_the_way

The parcel is on the way to the recipient (status Start Delivery / Out for Delivery).

Payload Examples
order.completed

The delivery succeeded (status Successful).

Payload Examples
order.failed

The delivery attempt failed (Redelivered Later, Need Rescheduled, Rejected by Recipient).

Payload Examples
order.cancelled

The order was cancelled.

Payload Examples
order.ready

Staff (or a driver, when allowed) marked the order ready for pickup (Dispatch Options → ready for pickup).

Payload Examples
driver.on_duty_changed

A driver went on or off duty in the app (driver duty option).

Payload Examples
driver.location_update

A driver position from the app or tracker, throttled per driver by driver_location_min_interval_sec (default 60 s). Sent to driver_location_webhook_url only.

Payload Examples

How to Configure: Settings → Webhooks (or GET/PUT /api/v1/webhook-settings, GraphQL webhookSettingsUpdate): order_lifecycle_webhook_url receives every order.* event and driver.on_duty_changed; order_lifecycle_events narrows that to a comma-separated list; driver_location_webhook_url and driver_location_min_interval_sec control driver.location_update. Several URLs may be comma-separated. Deliveries appear in the webhook delivery log with reference_type order / driver.

Signature & Verification: Signed exactly like every other outbound webhook of your account: legacy Signature header plus X-Webhook-Id / X-Webhook-Timestamp / X-Webhook-Signature-V2 with your webhook_sign_secret. Retries reuse the same event_id — deduplicate on it.

Device Order Events

Opt-in events for parcels handled by your smart lockers, kiosks and smart drops: a parcel stored in a machine, collected, taken out by staff or past its collection deadline, and problems opened or resolved on it. Additive — no existing webhook changes, and nothing is sent until you configure device_order_webhook_url.

device_order.stored

A parcel was placed in the machine and is waiting for the next person (recipient, courier or operator, see data.device_order.next_actor). due_at is the collection deadline.

Payload Examples
device_order.collected

The parcel was taken out by the person it was waiting for — the recipient, the courier or staff emptying a smart drop.

Payload Examples
device_order.removed

Staff took the parcel out of the machine. removal_reason says why: overdue_return, handover, relay, anomaly or recovery.

Payload Examples
device_order.overdue

The parcel passed its due_at without being collected. It is still in the machine and its code still works; overdue_at is set and next_actor becomes operator.

Payload Examples
device_order.exception_opened

A problem was opened on the handling (for example door_left_open, deposit_unverified, item_missing, overdue). data.exception carries id, type, severity and status.

Payload Examples
device_order.exception_resolved

A person closed a problem on the handling. data.exception.status is resolved or dismissed and resolution_action says what was done.

Payload Examples

Payload Examples: data.device_order: id, kind, status, next_actor, device_type, device_id, device_name, grid_code, reference_number, order_id, external_order_id, due_at, overdue_at, stored_at, ended_at, removal_reason (times in ISO 8601, null until reached). Exception events add data.exception: id, type, severity, status, resolution_action. The pickup code is never included. event_id is DOE-<ledger event id> and stays the same on retries.

How to Configure: Settings → Webhooks (or GET/PUT /api/v1/webhook-settings): device_order_webhook_url receives every device_order.* event; device_order_events narrows that to a comma-separated list. Several URLs may be comma-separated. Deliveries appear in the webhook delivery log with reference_type device_order.

Signature & Verification: Signed exactly like every other outbound webhook of your account: legacy Signature header plus X-Webhook-Id / X-Webhook-Timestamp / X-Webhook-Signature-V2 with your webhook_sign_secret. Retries reuse the same event_id — deduplicate on it.

Parcels a partner carrier delivers into your lockers under its own account are not sent on this channel; the partner receives them through its provider locker webhooks.

How to Configure

You can configure webhooks at two levels: at the business level (covers everything) or per customer (overrides for that specific B2B sub-account).

1. Get to the settings

Log in and go to Settings → API & Webhooks. Customer-level overrides are on the customer detail page.

2. Set the signing secret

Pick any string at least 16 characters long, ideally 32+ random bytes. This secret is what your receiver will use to verify signatures.

3. Set the event URLs you want

Fill in only the URLs for events you care about. Leave the rest blank to skip them.

webhook_sign_secretYou configure a shared secret on the settings page. Every outgoing webhook is signed with that secret. Your receiver recomputes the signature and compares — if they match, the payload is genuine and untampered.
order_create_webhook_urlFires whenever a local-delivery order (Delivery / Pickup / P2P) is created via any path — web form, REST/GraphQL API, ecommerce platform sync, automatic rules, import rows, etc. Excludes label-service and other non-delivery order types. Skipped in batch flow when order_create_async_postback_url is also configured for the same recipient. Configure with order_create_webhook_url.
order_status_change_webhook_urlFires on every order status transition — picked up, in transit, delivered, exception, cancelled. Configure with order_status_change_webhook_url.
tracking_event_webhook_urlFires on each tracking lifecycle event recorded against a package (information submitted, start delivery, delivery success, not delivered, etc.). Configure with tracking_event_webhook_url. Delivered and picked-up events also carry the proof of delivery: proof_files plus proof_files_detail (file_id, type, url, full_url, signed download URL). Photos uploaded after the event arrive as pod.files_updated. Each file also carries its event context: tracking_event_id, tracking_event_status_id, tracking_event_key, service_type (1 = delivery / 2 = pickup) and service_status (1 = success / 2 = failed), all null for legacy files without a recorded event.
order_create_async_postback_urlFires once after a batch order import finishes processing. Payload contains the per-row results array. Configure with order_create_async_postback_url.
pod_files_webhook_urlFired when a delivery photo or signature is added, replaced or removed (action: added / updated / removed) — one delivery per file, no more polling for attachments. Opt-in via pod_files_webhook_url. Each file also carries its event context: tracking_event_id, tracking_event_status_id, tracking_event_key, service_type (1 = delivery / 2 = pickup) and service_status (1 = success / 2 = failed), all null for legacy files without a recorded event. When staff replace a photo or signature, or set an archived version as current, the file is sent with action: updated and carries only the current image; archived versions are never sent.
order_deleted_webhook_urlFired when an order is permanently deleted, so your system can mirror the removal. Opt-in via order_deleted_webhook_url.
order_cancel_failed_webhook_urlFired when a cancel attempt is rejected (for example the order is already out for delivery), so your ops pipeline can monitor failed cancels without polling API responses. Opt-in via order_cancel_failed_webhook_url.
route_board_webhook_urlFired whenever a Route Board seat changes hands or the board changes state — action tells you what happened (claimed, standby, pooled, promoted, withdrawn, vetoed, replaced, assigned, awarded, lost, displaced, settled, board_opened, board_closed, board_cancelled). Business-level only. Opt-in via route_board_webhook_url.
device_order_webhook_urlOpt-in events for parcels handled by your smart lockers, kiosks and smart drops: a parcel stored in a machine, collected, taken out by staff or past its collection deadline, and problems opened or resolved on it. Additive — no existing webhook changes, and nothing is sent until you configure device_order_webhook_url.
Integration Updates Guide

What’s new in the API and webhooks — idempotent cancel, reconciliation feeds, v2 signatures, new events — with copy-paste examples. Everything is fully backward compatible.

Signature & Verification

Every outgoing webhook carries a hex-encoded HMAC-SHA256 signature in the request header. Your receiver must recompute the signature over the raw request body using the shared secret, and reject the request if it does not match.

Algorithm
HMAC-SHA256 (hex)
Header name
Signature
Verification steps
  1. Read the raw request body before any parsing or middleware modifies it.
  2. Compute hash_hmac('sha256', rawBody, sharedSecret) and hex-encode the result.
  3. Compare with the Signature header using a constant-time comparison (hash_equals in PHP, crypto.timingSafeEqual in Node).
  4. Return 2xx only if signatures match. Return 401 otherwise.
<?php $rawBody = file_get_contents('php://input'); $received = $_SERVER['HTTP_SIGNATURE'] ?? ''; $expected = hash_hmac('sha256', $rawBody, $sharedSecret); if (!hash_equals($expected, $received)) { http_response_code(401); exit('Bad signature'); } $payload = json_decode($rawBody, true); // ... handle event ... http_response_code(200); echo 'ok';
const crypto = require('crypto'); const express = require('express'); const app = express(); app.use('/webhooks/superroute', express.raw({ type: 'application/json' }), (req, res) => { const received = req.header('Signature') || ''; const expected = crypto.createHmac('sha256', sharedSecret) .update(req.body).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) { return res.status(401).send('Bad signature'); } const payload = JSON.parse(req.body.toString()); // ... handle event ... res.status(200).send('ok'); });
import hmac, hashlib from flask import Flask, request, abort app = Flask(__name__) @app.route('/webhooks/superroute', methods=['POST']) def webhook(): received = request.headers.get('Signature', '') expected = hmac.new(shared_secret.encode(), request.data, hashlib.sha256).hexdigest() if not hmac.compare_digest(received, expected): abort(401) payload = request.get_json() # ... handle event ... return 'ok', 200
func handleWebhook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) received := r.Header.Get("Signature") mac := hmac.New(sha256.New, []byte(sharedSecret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(received), []byte(expected)) { w.WriteHeader(401); return } // ... handle event ... w.WriteHeader(200) w.Write([]byte("ok")) }
require 'openssl' require 'rack/utils' post '/webhooks/superroute' do raw = request.body.read received = request.env['HTTP_SIGNATURE'] || '' expected = OpenSSL::HMAC.hexdigest('sha256', shared_secret, raw) halt 401 unless Rack::Utils.secure_compare(received, expected) payload = JSON.parse(raw) # ... handle event ... status 200 'ok' end

Formats and Standard Webhooks

Each webhook URL can receive events in the native format or as CloudEvents, and can also carry Standard Webhooks signature headers. Set this per URL in the webhook settings. A URL without a setting receives events exactly as before.

ce-id and webhook-id carry the event's own event_id when the body has one, otherwise X-Webhook-Event-Id. X-Webhook-Event-Id itself does not change. ce-source and ce-srbusiness name the business, also for deliveries to its customers.

Partner locker delivery and third-party delivery webhooks offer the same choice per endpoint, on the provider webhook settings page. Loading plan and Open Platform callbacks take it per request: callback_format and callback_signature, or callback.format and callback.signature. For them, ce-id and webhook-id are the event's own id (X-Webhook-Id, X-Webhook-Event-Id or X-Open-Delivery), ce-source names the sending business, and for 24 hours after a provider key rotation webhook-signature carries one signature per key.

Dataset webhooks offer the same choice per webhook, on the dataset webhook form and in its API (event_format and standard_signature). Standard Webhooks uses the webhook's secret key, so it needs one. Dataset webhook URLs must be public HTTPS addresses.

ce-specversion: 1.0 ce-id: 4c1a7f92-0b6d-4e11-9c3a-2f7b5d8e6a10 ce-source: https://admin.superroute.ca/webhook-sources/tenant/1234 ce-type: ca.superroute.order.status_change ce-subject: order/1001 ce-time: 2026-09-23T12:00:00-04:00 ce-dataschema: https://admin.superroute.ca/docs/asyncapi-webhooks.json#/components/messages/tenant.order.status_change/payload ce-srbusiness: 1234 ce-srattempt: 1
Standard Webhooks

With Standard Webhooks selected, each delivery also carries webhook-id, webhook-timestamp and webhook-signature. They are signed again on every retry, so a retry is within a 5-minute tolerance. The key is your Webhook Sign Secret in whsec_ form, shown on the settings page. The native headers are still sent.

webhook-id: 4c1a7f92-0b6d-4e11-9c3a-2f7b5d8e6a10 webhook-timestamp: 1790179200 webhook-signature: v1,<base64(HMAC-SHA256(webhook-id + "." + webhook-timestamp + "." + body))>
<?php // PHP, no library: the key is the whsec_ secret, base64-decoded. $key = base64_decode(substr($whsecSecret, strlen('whsec_'))); $rawBody = file_get_contents('php://input'); $id = $_SERVER['HTTP_WEBHOOK_ID']; $ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP']; if (abs(time() - (int) $ts) > 300) { http_response_code(401); exit; } $expected = base64_encode(hash_hmac('sha256', $id . '.' . $ts . '.' . $rawBody, $key, true)); $valid = false; foreach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE']) as $entry) { [$version, $signature] = array_pad(explode(',', $entry, 2), 2, ''); $valid = $valid || ($version === 'v1' && hash_equals($expected, $signature)); } if (!$valid) { http_response_code(401); exit; }
// Node.js — npm install standardwebhooks import { Webhook } from "standardwebhooks"; const payload = new Webhook(whsecSecret).verify(rawBody, req.headers);
# Python — pip install standardwebhooks from standardwebhooks.webhooks import Webhook payload = Webhook(whsec_secret).verify(raw_body, request.headers)
Headers by sender

Each sender signs differently. The header name X-Webhook-Signature is used by three senders with three different recipes; verify with the recipe of the sender that called you.

SenderHeadersSignature
Tenant webhooks (this page) Content-Type, Signature, X-Webhook-Event-Id, X-Webhook-Timestamp, X-Webhook-Signature-V2 Signature = hex(HMAC-SHA256(body)) X-Webhook-Signature-V2 = hex(HMAC-SHA256(timestamp + "." + body))
Partner locker deliveries Content-Type, X-Webhook-Id, X-Webhook-Delivery-Id, X-Webhook-Timestamp, X-Webhook-Key-Id, X-Webhook-Content-Digest, X-Webhook-Signature, X-Webhook-Test X-Webhook-Signature = "v1=" + base64(HMAC-SHA256(timestamp + "\n" + delivery_id + "\n" + body))
Third-party delivery assignments Content-Type, X-Webhook-Id, X-Webhook-Delivery-Id, X-Webhook-Timestamp, X-Webhook-Key-Id, X-Webhook-Content-Digest, X-Webhook-Signature X-Webhook-Signature = "v1=" + base64(HMAC-SHA256(timestamp + "\n" + delivery_id + "\n" + body))
Loading plan callbacks Content-Type, X-Webhook-Event-Id, X-Webhook-Event-Type, X-Webhook-Timestamp, X-Webhook-Signature X-Webhook-Signature = "v1=" + hex(HMAC-SHA256(timestamp + "." + body))
Open Platform job callbacks Content-Type, X-Open-Event, X-Open-Delivery, X-Open-Job, X-Open-Timestamp, X-Open-Signature X-Open-Signature = "v1=" + hex(HMAC-SHA256(timestamp + "." + body))
OHP push Content-Type, X-Ohp-Event, X-Ohp-Delivery, X-Ohp-Timestamp, X-Ohp-Signature
Dataset webhooks Content-Type, X-Webhook-Event, X-Webhook-Timestamp, X-Webhook-Signature X-Webhook-Signature = hex(HMAC-SHA256(body))
Tenant webhooks with Standard Webhooks selected webhook-id, webhook-timestamp, webhook-signature webhook-signature = "v1," + base64(HMAC-SHA256(webhook-id + "." + webhook-timestamp + "." + body))

Price Hiding

Your service provider can hide its prices from a customer account, per order family (Local Delivery, Label Service, Shipping Service, LTL Service, Storage Services, Moving Services, device orders). When that applies to you, deliveries to your endpoint carry no price fields: shipping_price, price_details, currency, tax, surcharge amounts, rate prices and the like are omitted rather than sent as zero. Rate lists keep their rate_id and service names so a service can still be chosen.

Nothing else changes. Event types, every other field, the signature and the retry rules are exactly as documented. Treat the price fields as optional and never assume that a missing figure means free.

Retry & Reliability

Your endpoint should respond with a 2xx status quickly. If it returns anything else, times out, or is unreachable, the delivery is retried.

Max attempts
5 (initial + 4 retries)
Per-attempt timeout
3 seconds
Backoff
Exponential — roughly 10s, 100s, 1000s, 10000s between attempts
Build for idempotency. Because a delivery can be retried, your receiver may see the same event more than once. Use the order id / tracking number as a dedupe key — store processed event IDs for at least 24 hours.
Recommended response. Acknowledge fast (HTTP 200) and process asynchronously on your side. Avoid doing slow work synchronously inside the webhook handler — it will hit our 3-second timeout.

Signature Verifier

Paste a payload you received from us along with the Signature header value and your secret — this tool recomputes the signature in your browser (nothing leaves this page) and tells you if it matches.

Send a Test Webhook

Fire a real, properly-signed webhook from our server to a URL you provide. Use this to test that your receiver is reachable, that you parse the payload correctly, and that your signature verification logic works.

Recent Webhook Deliveries

View the most recent webhook delivery attempts on your account — both real production events and tests sent from this page. Paste your Bearer token to load.

Delivery records are kept for 90 days.

A manual redelivery gets a new X-Webhook-Event-Id. An event_id that the event itself carries (pod.files_updated, order.deleted, lifecycle and device order events) keeps its original value.

Time Event URL Status HTTP Attempt Time (ms) Test? Actions
No webhook deliveries found yet.

Best Practices