Webhooks
Webhooks let you receive real-time HTTP notifications when delivery events occur, so you do not need to poll the API for status changes.
When to use webhooks
Register a webhook
/v1/webhooksRegister a new webhook endpoint to receive delivery event notifications.
urlstringrequiredThe HTTPS URL where Zippex will send POST requests. Must use HTTPS.
eventsstring[]requiredArray of event types to subscribe to. Use ["*"] to receive all events.
descriptionstringoptionalA label for your own reference.
curl -X POST https:"color:#6a9955">//api.zippex.com/v1/webhooks \
-H "Authorization: Bearer zx_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/zippex",
"events": ["delivery.created", "delivery.delivered", "delivery.cancelled"],
"description": "Production delivery updates"
}'{
"id": "wh_x1y2z3a4",
"url": "https://yourapp.com/webhooks/zippex",
"events": [
"delivery.created",
"delivery.delivered",
"delivery.cancelled"
],
"description": "Production delivery updates",
"secret": "whsec_b5c6d7e8f9g0h1i2j3k4l5m6",
"active": true,
"created_at": "2026-03-10T14:00:00Z"
}Save your webhook secret
secret is only returned once and is required to verify webhook signatures. Store it securely in your environment variables.List webhooks
/v1/webhooksList all registered webhook endpoints.
curl https:"color:#6a9955">//api.zippex.com/v1/webhooks \
-H "Authorization: Bearer zx_live_your_api_key"{
"data": [
{
"id": "wh_x1y2z3a4",
"url": "https://yourapp.com/webhooks/zippex",
"events": [
"delivery.created",
"delivery.delivered",
"delivery.cancelled"
],
"description": "Production delivery updates",
"active": true,
"created_at": "2026-03-10T14:00:00Z"
}
]
}Delete a webhook
/v1/webhooks/{webhookId}Delete a webhook endpoint.
curl -X DELETE https:"color:#6a9955">//api.zippex.com/v1/webhooks/wh_x1y2z3a4 \
-H "Authorization: Bearer zx_live_your_api_key"{
"id": "wh_x1y2z3a4",
"deleted": true
}Webhook events
| Event | Triggered when |
|---|---|
| delivery.created | A new delivery is created. |
| delivery.driver_assigned | A driver has accepted the delivery. |
| delivery.picked_up | The driver has collected the package. |
| delivery.in_transit | The driver is en route to the dropoff. |
| delivery.delivered | The package has been delivered. |
| delivery.cancelled | The delivery has been cancelled. |
| delivery.failed | The delivery attempt failed (e.g., recipient unavailable after retries). |
Webhook payload format
Zippex sends a POST request with a JSON body to your webhook URL:
{
"id": "evt_q1w2e3r4",
"type": "delivery.delivered",
"created_at": "2026-03-10T14:45:00Z",
"data": {
"id": "del_m8n9o0p1",
"status": "delivered",
"fee": 1249,
"currency": "cad",
"pickup": {
"address": "350 W Georgia St, Vancouver, BC V6B 6B1",
"name": "Coastal Bites"
},
"dropoff": {
"address": "2085 Main St, Vancouver, BC V5T 3C3",
"name": "Jane Smith"
},
"metadata": {
"order_id": "ORD-4521"
},
"delivered_at": "2026-03-10T14:45:00Z"
}
}Your endpoint must return a 2xx status within 10 seconds to acknowledge receipt.
Zippex should treat your endpoint as an event sink, not a request-response integration point. Persist the event quickly, acknowledge it, and do heavier processing asynchronously.
Signature verification
Every webhook request includes a Zippex-Signature header. Verify this signature to confirm the request came from Zippex and was not tampered with.
The signature is an HMAC-SHA256 hex digest of the raw request body, using your webhook secret as the key. The header format is:
Zippex-Signature: t=1710079500,v1=5d3b8a...t is the Unix timestamp when the signature was generated. v1 is the HMAC computed over {t}.{raw_body}.
const crypto = require('crypto');
const WEBHOOK_SECRET = process.env.ZIPPEX_WEBHOOK_SECRET; "color:#6a9955">// whsec_...
function verifyZippexSignature(req, res, next) {
const signature = req.headers['zippex-signature'];
if (!signature) {
return res.status(400).json({ error: 'Missing signature header' });
}
const parts = Object.fromEntries(
signature.split(',').map(part => part.split('='))
);
const timestamp = parts.t;
const receivedSig = parts.v1;
"color:#6a9955">// Reject requests older than 5 minutes to prevent replay attacks
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(400).json({ error: 'Signature too old' });
}
const payload = `${timestamp}.${req.rawBody}`;
const expectedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(receivedSig), Buffer.from(expectedSig))) {
return res.status(400).json({ error: 'Invalid signature' });
}
next();
}
"color:#6a9955">// Usage with Express (make sure to capture raw body)
const express = require('express');
const app = express();
app.post(
'/webhooks/zippex',
express.json({
verify: (req, _res, buf) => { req.rawBody = buf.toString(); },
}),
verifyZippexSignature,
(req, res) => {
const event = req.body;
console.log(`Received ${event.type} for delivery ${event.data.id}`);
"color:#6a9955">// Handle the event
switch (event.type) {
case 'delivery.delivered':
"color:#6a9955">// Update your order status
break;
case 'delivery.cancelled':
"color:#6a9955">// Handle cancellation
break;
}
res.json({ received: true });
}
);Always verify the raw request body before parsing or mutating it. Signature checks will fail if your framework rewrites the payload first.
Retry policy
If your endpoint does not return a 2xx status within 10 seconds, Zippex retries the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 10 minutes |
| 3rd retry (final) | 1 hour |
After 3 failed attempts, the webhook is marked as failed. Persistent failures (over 90% failure rate for 24 hours) will cause the webhook to be automatically disabled. You can re-enable it from the dashboard.
Next step
After webhook handling is in place, review the Sandbox guide for test-mode behavior and the Rate Limits page for retry strategy.