Get started
Webhooks
Nodium calls you when something happens. Signed, retried, logged.
What you receive
{ "id": "evt_5f1c0a9e2b7d4c3a8e6f1b2c3d4e5f60", "event": "message.received", "workspace_id": "…", "occurred_at": "2026-09-18T09:12:44.120Z", "data": { "message_id": "…", "conversation_id": "…", "contact_id": "…", "channel_type": "whatsapp", "type": "text", "text": "Hello, is my order ready?" }}idis the event's own id: it stays the same when a delivery is retried or redelivered. Deduplicate on it.- Headers:
X-Nodium-Event(the event name),X-Nodium-Delivery(this delivery),X-Nodium-Signature. - Answer with any
2xxwithin 10 seconds. Do the work afterwards.
Events
Verify the signature
X-Nodium-Signature: t=<seconds>,v1=<hex>. The signature is HMAC-SHA256(secret, "<t>.<raw body>"). During a secret rotation the header carries two v1= values — accept the call if any matches. Reject calls older than 5 minutes.
import crypto from 'node:crypto'// rawBody: the request body exactly as received, before any JSON parsing.export function verifyNodium(rawBody, header, secret) { const parts = header.split(',') const t = parts.find(p => p.startsWith('t='))?.slice(2) const signatures = parts.filter(p => p.startsWith('v1=')).map(p => p.slice(3)) if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex') return signatures.some(sig => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))}import hmac, hashlib, timedef verify_nodium(raw_body: bytes, header: str, secret: str) -> bool: parts = header.split(",") t = next((p[2:] for p in parts if p.startswith("t=")), None) signatures = [p[3:] for p in parts if p.startswith("v1=")] if not t or abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(sig, expected) for sig in signatures)Retries, log, redelivery
- A call that fails or does not answer
2xxis retried after 1 min, 5 min, 30 min, 2 h and 12 h — six attempts in all — then markedfailed. GET /webhooks/deliverieslists every delivery with the status your server answered and the error. That settles "we never received it".POST /webhooks/deliveries/{id}/redeliversends it again, same payload, sameid.POST /webhooks/{id}/rotate-secretissues a new secret; the old one keeps signing for 24 hours so you can deploy without missing a call.POST /webhooks/{id}/testfires one signed test call now and tells you what came back.