Webhooks
Instead of asking ZargoLogix whether something changed, let ZargoLogix tell you: we send a signed JSON message to your server the moment it happens.
Setting up an endpoint
A ZargoLogix administrator adds your endpoint in the Integration Center, on the Webhooks tab: the URL, and which events it should receive (or all of them). The signing secret, which starts with whsec_, is shown when the endpoint is created or its secret is rotated.
- The URL must be https and must point to a public server. Addresses on private or internal networks are refused, and checked again before every delivery.
- The Send test event button sends a
pingright away and reports exactly what your server answered, so you can fix a problem in minutes.
What we send
Every delivery is an HTTP POST with a JSON body in the same envelope, whatever the event:
{
"id": "evt_6f1c2a52-8c0d-4e4f-9d5b-3f1b2a9c7e10",
"type": "shipment.status_changed",
"created": "2026-10-10T12:00:00.000Z",
"data": {
"shipment": { "id": "cm2k9x0a10001", "tracking_id": "ZLG90215772410", "status": "IN_TRANSIT", "...": "..." },
"previous_status": "PICKED_UP",
"note": null
}
}The objects inside data are the same ones the data API returns (see Shipments, Invoices and Quotes), so one parser handles both.
| Header | Type | Meaning |
|---|---|---|
| ZargoLogix-Signature | string | The signature to verify. Looks like t=1760097600,v1=ab12…. |
| ZargoLogix-Event-Id | string | The envelope id. The same for every retry of the same event. |
| ZargoLogix-Event-Type | string | The event type, for example shipment.status_changed. |
| ZargoLogix-Delivery-Id | string | Identifies this delivery. |
| ZargoLogix-Delivery-Attempt | integer | 1 for the first try, 2 for the first retry, and so on. |
Verify the signature
The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw body, using your whsec_ secret. Reject anything whose timestamp is more than five minutes from your clock, which stops an old request being replayed.
const crypto = require('crypto');
// secret = the whsec_... value shown when the endpoint was created or its secret rotated
// rawBody = the request body EXACTLY as received, before any JSON parsing
function verify(secret, header, rawBody, toleranceSec = 300) {
const p = Object.fromEntries(header.split(',').map((s) => s.trim().split('=')));
if (!p.t || !p.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(p.t)) > toleranceSec) return false; // replay protection
const expected = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(p.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// The header is called ZargoLogix-Signatureimport hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, tolerance=300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Delivery and retries
- Answer with any 2xx status within 8 seconds. Do your real work after answering.
- Redirects are not followed and count as failures.
- A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: seven attempts in all.
- Reply
410 Goneto tell us to stop sending to that endpoint. We disable it straight away. - An endpoint that fails 25 deliveries in a row is disabled automatically. An administrator can fix the problem and turn it back on.
- The same event can arrive more than once (retries, or a manual replay). Store the envelope
idand ignore ones you have already handled. - Events are not guaranteed to arrive in the order they happened. Use
created, or fetch the record, if order matters.
Events
| Event | Payload (data) | When it fires |
|---|---|---|
| booking.received | shipment | A booking was submitted (from the website, or by a customer in the portal). |
| shipment.status_changed | shipment, previous_status, note | A shipment moved to a new status. Fires for every status change, including ones that send no email. |
| invoice.created | invoice | An invoice was issued. |
| payment.received | payment, invoice | A payment was recorded against an invoice. |
| quote.status_changed | quote, previous_status | A quote changed status (sent, accepted, rejected, expired, converted). |
| ticket.created | ticket | A support ticket was opened. The message text is deliberately not included. |
| customs.hold_placed | hold, shipment | A customs hold was placed on a shipment. |
| document.generated | document | A document such as an invoice PDF or bill of lading was generated. Generated documents are private: file_private is true and there is no download link. |
| ping | message | Sent only when an administrator presses Send test event. |