NodukanNodukan/ Developers
Introduction

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#

EventWhen it fires
lead.capturedA visitor submits a lead magnet form
lead.magnet.updatedA lead magnet's settings or status changes
chat.newA new chat conversation is started by a buyer
chat.message.receivedA new message arrives in an existing conversation
post.publishedA scheduled social post is successfully published
post.failedA scheduled post fails to publish (e.g., token expired)
short_url.clickedA short URL is clicked (high-volume — use judiciously)
automation.triggeredA social automation rule fires
subscription.updatedThe 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:

ComponentDescription
tUnix timestamp (seconds) when the event was dispatched
v1HMAC-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
Warning:

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:

AttemptDelay
1stImmediate
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"]}'
Note:

Use NDK_test_ keys and test webhook endpoints during development. Test events are clearly marked in the payload and don't affect production data.

Was this helpful?
Edit on GitHub

Last updated