Skip to content
ZargoLogix

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.

What we send

Every delivery is an HTTP POST with a JSON body in the same envelope, whatever the event:

Request body (example)
{
  "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.

HeaderTypeMeaning
ZargoLogix-SignaturestringThe signature to verify. Looks like t=1760097600,v1=ab12….
ZargoLogix-Event-IdstringThe envelope id. The same for every retry of the same event.
ZargoLogix-Event-TypestringThe event type, for example shipment.status_changed.
ZargoLogix-Delivery-IdstringIdentifies this delivery.
ZargoLogix-Delivery-Attemptinteger1 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.

Node.js
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-Signature
Python
import 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

Events

EventPayload (data)When it fires
booking.receivedshipmentA booking was submitted (from the website, or by a customer in the portal).
shipment.status_changedshipment, previous_status, noteA shipment moved to a new status. Fires for every status change, including ones that send no email.
invoice.createdinvoiceAn invoice was issued.
payment.receivedpayment, invoiceA payment was recorded against an invoice.
quote.status_changedquote, previous_statusA quote changed status (sent, accepted, rejected, expired, converted).
ticket.createdticketA support ticket was opened. The message text is deliberately not included.
customs.hold_placedhold, shipmentA customs hold was placed on a shipment.
document.generateddocumentA 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.
pingmessageSent only when an administrator presses Send test event.