Hazchem On The Go
Hazchem Connect Developers

Webhooks

Webhooks push every state change to your system, so you never poll for tracking. Each delivery is signed with HMAC-SHA256 over the timestamp and body.

Envelope

Every event carries an event id, type, created time, tenant, job id and a data object. Respond with any 2xx within 10 seconds; failures retry with exponential back-off and appear in the delivery log for manual replay. Events can arrive more than once: de-duplicate on id.

Example delivery
POST /hazchem/events HTTP/1.1
content-type: application/json
hazchem-timestamp: 1791795600
hazchem-signature: v1=5f0c2a...e91b
hazchem-event-id: evt_2b7d9c41

{
  "id": "evt_2b7d9c41",
  "type": "delivery.completed",
  "created_at": "2026-10-12T10:41:00.000Z",
  "tenant_id": "t-lab",
  "job_id": "job-sbx1",
  "data": { "recipient": "Receiving desk", "otp_verified": true }
}

Events

EventMeaningdata example
job.createdJob booked through any channel{"job_id":"job-sbx1","ref":"HZ-SBX1001","sku":"HZ-LAB-05","sla_due":"2026-10-12T11:00:00.000Z"}
job.assignedDriver and vehicle assigned{"job_id":"job-sbx1","driver":{"first_name":"Sipho"},"vehicle":{"registration":"THE GO 40 GP"},"eta_pickup_minutes":18}
pickup.completedCollected; scan count and box temperature{"job_id":"job-sbx1","parcels_scanned":2,"seal":"HZ-S445330","box_temperature_c":4.6}
custody.scannedScan at any hand-off with seal check{"job_id":"job-sbx1","location":"Meridian Central Lab","seal_match":true,"by":"Receiving desk"}
temperature.excursionOut of range with duration and action{"job_id":"job-23","range_c":[2,8],"max_c":9.4,"minutes_out_of_range":14,"action":"Receiver notified, re-dispatch offered"}
delivery.completedDelivered with recipient{"job_id":"job-sbx1","recipient":"Receiving desk","otp_verified":true,"delivered_at":"2026-10-12T10:41:00.000Z"}
delivery.failedHandover failed; return started{"job_id":"job-30","reason":"No person over 14 with the OTP","return_job_id":"ret_job-30"}
pod.availablePOD artefacts ready{"job_id":"job-sbx1","pod_url":"https://api.haz-chem.co.za/v1/jobs/job-sbx1/pod"}
invoice.issuedInvoice issued{"invoice_id":"INV-2618","period":"2026-09","amount_cents":41280000,"currency":"ZAR","vat_inclusive":false}

Also available: quote.created, job.updated, job.cancelled, driver.en_route, pickup.arriving, pickup.failed, seal.broken, temperature.reading, eta.changed, sla.at_risk, sla.breached, delivery.arriving, return.created, return.completed, certificate.issued, statement.issued, payout.sent, incident.reported, compliance.expiring.

Verify the signature

Compute HMAC-SHA256(secret, timestamp + "." + raw_body) and compare it in constant time with the Hazchem-Signature header. Reject timestamps older than five minutes to stop replays. Rotate secrets from the developer console; the old secret stays valid for 24 hours.

Node.js
import crypto from "node:crypto";

// Express-style handler. Use the RAW body: re-serialised JSON will not match the signature.
export function verifyHazchem(rawBody, headers, secret) {
  const ts = headers["hazchem-timestamp"];
  const sig = headers["hazchem-signature"];          // "v1=<hex>"
  if (!ts || !sig) return false;
  // Reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const given = sig.replace(/^v1=/, "");
  return given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
}
Python
import hmac, hashlib, time

def verify_hazchem(raw_body: bytes, headers: dict, secret: str) -> bool:
    ts = headers.get("Hazchem-Timestamp")
    sig = headers.get("Hazchem-Signature", "").removeprefix("v1=")
    if not ts or abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)