Outbound webhooks
ProtectPlus can POST every tracking event to a merchant-controlled URL with an HMAC signature so that downstream systems (OMS, warehouse management, ticketing, custom notification engines) can react in real time without polling our API.
This is the inverse of the inbound (Path B / OMS push) integration documented in the Integration sub-tab: there, the merchant POSTs shipments to P+; here, P+ POSTs canonical tracking events back to the merchant.
Configuring
In the merchant dashboard:
- Tracking → Settings → Integration tab
- Scroll to Forward updates to your endpoint
- Save your https URL (http is rejected — we sign over the wire)
- Click Generate secret and copy the value (shown exactly once)
- Click Send test delivery to smoke-test your receiver
- Toggle Enable to start receiving live events
Test deliveries fire even when the toggle is off — they're meant for pre-flight validation before going live.
Payload shape
P+ POSTs application/json with this envelope:
{
"event_type": "delivered",
"delivery_id": "65f2c1ab2c4d5e6f7a8b9c01",
"delivered_at": "2026-05-12T10:34:01.234Z",
"payload_version": 1,
"data": {
"shipment_id": "65f2c0...",
"store_id": "65f2bf...",
"order_id": "65f2bf...",
"order_number": "1024",
"customer_email": "[email protected]",
"tracking_number": "1Z999AA10123456784",
"carrier": "ups",
"tracking_url": "https://www.ups.com/track?...",
"canonical_event": "delivered",
"canonical_status": "DELIVERED",
"canonical_substatus": null,
"canonical_label": "Delivered",
"timeline_step": "delivered",
"shipped_at": "2026-05-10T08:00:00.000Z",
"delivered_at": "2026-05-12T10:33:55.000Z",
"occurred_at": "2026-05-12T10:34:00.000Z",
"tracking_status_details": { "...": "..." }
}
}Event types
The event_type field on the envelope is one of:
| Event | Meaning |
|---|---|
shipment_created | Label printed but not yet scanned (PRE_TRANSIT) |
shipped | First in-transit scan |
out_for_delivery | On the truck for final delivery |
delivered | Confirmed delivery scan |
exception | Delivery failure / address issue / returned to sender |
test | Synthetic event from the Send test delivery button |
Each canonical event fires at most once per shipment — the dispatcher de-duplicates on the same internal merchant_event_history ledger that drives P+'s own notification engine.
HMAC verification
Every request carries an X-PPLUS-Signature header in the form sha256=<hex> computed as HMAC-SHA-256 over the raw request body using your secret.
Node.js
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const [scheme, hex] = (signatureHeader || '').split('=');
if (scheme !== 'sha256' || !hex) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
// Constant-time compare to avoid timing attacks
const a = Buffer.from(hex, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Python
import hashlib, hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header or '=' not in signature_header:
return False
scheme, hex_sig = signature_header.split('=', 1)
if scheme != 'sha256':
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(hex_sig, expected)Important — your framework MUST give you the raw request body bytes, not a re-serialised version. Express needs the
verifycallback onbody-parser(see our ownglobal.packages.jsfor an example); FastAPI needsawait request.body()before parsing JSON.
Retry policy
If your endpoint returns a non-2xx status or the request times out (10s), the delivery is retried with exponential backoff:
| Attempt | Delay since previous |
|---|---|
| 1 | (immediate) |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
After the 6th attempt the delivery is marked failed (dead-letter) and will not be retried automatically. The merchant dashboard surfaces dead-lettered deliveries in the Recent deliveries strip with a red Failed pill.
Idempotency on your side
The delivery_id field on every payload is unique per delivery row in our database. If the same delivery_id arrives twice (e.g. our network hiccup made us think your 2xx response failed), treat it as the same event and respond with another 2xx — your processing logic should be idempotent on delivery_id.
The data.shipment_id + data.canonical_event pair is also a stable deduplication key per shipment lifecycle event.
Headers reference
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-PPLUS-Signature | sha256=<hex> HMAC over raw body |
X-PPLUS-Event | Same value as event_type in the body |
X-PPLUS-Delivery-Id | Same value as delivery_id in the body |
User-Agent | ProtectPlus-Webhooks/1.0 |
The signature, event, and delivery_id are duplicated in headers so a thin forwarder / log aggregator can route on them without parsing JSON.
Rotating your secret
Hit Regenerate in the dashboard. The previous secret is invalidated immediately — any delivery in flight signed with the old value will fail verification on your side until you cycle to the new value. Coordinate the rotation with your deployment to minimise the missed-event window.