Public beta

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

Use webhooks for backend workflows like order state changes, notifications, and reconciliation. Use the tracking endpoint only when you need a live map or ETA in a client experience.

Register a webhook

POST/v1/webhooks

Register a new webhook endpoint to receive delivery event notifications.

urlstringrequired

The HTTPS URL where Zippex will send POST requests. Must use HTTPS.

eventsstring[]required

Array of event types to subscribe to. Use ["*"] to receive all events.

descriptionstringoptional

A 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"
  }'
201 Created
{
  "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

The secret is only returned once and is required to verify webhook signatures. Store it securely in your environment variables.

List webhooks

GET/v1/webhooks

List all registered webhook endpoints.

curl https:"color:#6a9955">//api.zippex.com/v1/webhooks \
  -H "Authorization: Bearer zx_live_your_api_key"
200 OK
{
  "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

DELETE/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"
200 OK
{
  "id": "wh_x1y2z3a4",
  "deleted": true
}

Webhook events

EventTriggered when
delivery.createdA new delivery is created.
delivery.driver_assignedA driver has accepted the delivery.
delivery.picked_upThe driver has collected the package.
delivery.in_transitThe driver is en route to the dropoff.
delivery.deliveredThe package has been delivered.
delivery.cancelledThe delivery has been cancelled.
delivery.failedThe 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:

Example payload
{
  "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:

AttemptDelay
1st retry1 minute
2nd retry10 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.