Webhooks
Receive real-time push notifications from Nodukan — event types, payload format, HMAC-SHA256 signing, retry policy, and best practices.
Webhooks
Webhooks let Nodukan push data to your server the moment something happens — a new lead is captured, a post is published, a chat arrives. Instead of polling the API every few seconds, you register an HTTPS endpoint and receive a signed POST request automatically.
Event types#
| Event | When it fires |
|---|---|
lead.captured | A visitor submits a lead magnet form |
lead.magnet.updated | A lead magnet's settings or status changes |
chat.new | A new chat conversation is started by a buyer |
chat.message.received | A new message arrives in an existing conversation |
post.published | A scheduled social post is successfully published |
post.failed | A scheduled post fails to publish (e.g., token expired) |
short_url.clicked | A short URL is clicked (high-volume — use judiciously) |
automation.triggered | A social automation rule fires |
subscription.updated | The creator's Nodukan plan changes |
Payload format#
Every webhook delivery is an HTTP POST with Content-Type: application/json. The body follows a consistent envelope:
{
"id": "evt_01j9x4km2v",
"event": "lead.captured",
"createdAt": "2026-05-06T10:15:30.123Z",
"data": {
"leadMagnetId": "lm_01j9x...",
"leadMagnetTitle": "Free Notion Template",
"lead": {
"id": "lead_01j9x...",
"email": "[email protected]",
"name": "Ravi Kumar",
"phone": "+91 98765 43210",
"capturedAt": "2026-05-06T10:15:29.900Z"
}
}
}
The data shape varies by event type. Refer to the Webhooks API reference for per-event payload schemas.
Signing and verification#
Every outbound webhook request includes a signature header:
X-Nodukan-Signature: t=1715000130,v1=3c5e9a2f7d4b1...
The header contains:
| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the event was dispatched |
v1 | HMAC-SHA256 hex digest of <t>.<raw_body> using your signing secret |
Verification — Node.js#
import crypto from 'crypto';
const SIGNING_SECRET = process.env.NODUKAN_WEBHOOK_SECRET; // whsec_...
export function verifyNodukansignature(rawBody, signatureHeader) {
if (!signatureHeader) return false;
// Parse header
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=')),
);
const { t, v1 } = parts;
if (!t || !v1) return false;
// Reject if timestamp is older than 5 minutes
const nowSeconds = Math.floor(Date.now() / 1000);
if (Math.abs(nowSeconds - Number(t)) > 300) return false;
// Compute expected signature
const signedPayload = `${t}.${rawBody}`;
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload, 'utf8')
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
}
Express handler example:
import express from 'express';
import { verifyNodukansignature } from './verify.js';
const app = express();
// Use express.raw() to get the raw body for signature verification
app.post('/webhooks/nodukan', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const sig = req.headers['x-nodukan-signature'];
if (!verifyNodukansignature(rawBody, sig)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
switch (event.event) {
case 'lead.captured':
// Queue async processing — don't do heavy work here
processLeadCapture(event.data);
break;
case 'chat.new':
processNewChat(event.data);
break;
default:
console.log(`Unhandled event: ${event.event}`);
}
// Respond quickly — Nodukan considers anything other than 2xx a failure
res.status(200).json({ received: true });
});
Verification — Python#
import hashlib, hmac, os, time
SIGNING_SECRET = os.environ["NODUKAN_WEBHOOK_SECRET"] # whsec_...
def verify_signature(raw_body: bytes, signature_header: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
return False
# Replay protection: reject events older than 5 minutes
if abs(time.time() - int(t)) > 300:
return False
signed_payload = f"{t}.{raw_body.decode('utf-8')}"
expected = hmac.new(
SIGNING_SECRET.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(v1, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/nodukan")
def webhook():
sig = request.headers.get("X-Nodukan-Signature", "")
if not verify_signature(request.get_data(), sig):
abort(401)
event = request.json
if event["event"] == "lead.captured":
process_lead.delay(event["data"]) # e.g., Celery task
return {"received": True}, 200
Always use the raw request body (before JSON parsing) for signature verification. Parsing and re-serializing JSON can change whitespace and break the HMAC.
Retry policy#
If your endpoint returns a non-2xx status code (or times out after 10 seconds), Nodukan retries delivery up to 4 times with an exponential schedule:
| Attempt | Delay |
|---|---|
| 1st | Immediate |
| 2nd | +1 minute |
| 3rd | +5 minutes |
| 4th | +30 minutes |
After all 4 attempts fail, the delivery is marked as failed. You can see failed deliveries and trigger manual re-delivery in Dashboard → Webhooks → Delivery Logs.
Best practices#
Respond quickly. Your endpoint must return a 2xx within 10 seconds. Offload heavy processing (database writes, CRM syncs, third-party API calls) to an async queue (Bull, Celery, SQS) and respond immediately.
Make handlers idempotent. Nodukan may deliver the same event more than once (e.g., during retries). Use the id field (e.g., evt_01j9x...) as an idempotency key — store processed event IDs and skip duplicates.
if (await redis.get(`evt:processed:${event.id}`)) {
return res.status(200).json({ received: true, duplicate: true });
}
await redis.set(`evt:processed:${event.id}`, '1', 'EX', 86400);
// ... process event
Verify signatures. Never process a webhook payload without verifying the HMAC signature. Unverified endpoints are vulnerable to spoofed events.
Monitor delivery health. Subscribe to post.failed and automation.triggered with the same webhook to catch operational errors early.
Local development with ngrok#
Nodukan webhooks require a publicly reachable HTTPS URL. During development, use ngrok to create a tunnel to your localhost:
# Install ngrok, then:
ngrok http 3000
# Copy the HTTPS URL it gives you, e.g.:
# https://abc123.ngrok.io
# Register it as a webhook endpoint:
curl -X POST https://api.nodukan.com/v1/webhooks \
-H "Authorization: Bearer NDK_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://abc123.ngrok.io/webhooks/nodukan", "events": ["lead.captured"]}'
Use NDK_test_ keys and test webhook endpoints during development. Test events are clearly marked in the payload and don't affect production data.
Last updated