Auto-DM Quickstart
Build a real-time Auto-DM bot using Nodukan webhooks — receive chat events, store recent conversations, and send contextual replies via API.
Auto-DM Quickstart
This guide walks you through building a simple but production-ready Auto-DM bot. When a new chat arrives on your creator's Instagram or storefront, you'll:
- Receive a signed webhook event
- Look up the buyer context
- Reply automatically via the Chats API
By the end you'll have a working Express.js bot that handles real-time chat events.
Prerequisites#
- Node.js 18+ and npm
- An ngrok account (for local testing)
- A Nodukan API key with
chats:read,chats:write, andwebhooks:writescopes - A registered webhook endpoint (we'll create it in this guide)
Step 1 — Scaffold the project#
mkdir nodukan-autodm-bot && cd nodukan-autodm-bot
npm init -y
npm install express dotenv
Create a .env file:
NODUKAN_API_KEY=NDK_live_YOUR_KEY_HERE
NODUKAN_WEBHOOK_SECRET=whsec_YOU_WILL_GET_THIS_BELOW
PORT=3000
Step 2 — Build the Express server#
Create server.js:
import 'dotenv/config';
import express from 'express';
import crypto from 'crypto';
const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.NODUKAN_WEBHOOK_SECRET;
const API_KEY = process.env.NODUKAN_API_KEY;
const API_BASE = 'https://api.nodukan.com/v1';
// ── Signature verification ─────────────────────────────────────────────────
function verifySignature(rawBody, signatureHeader) {
if (!signatureHeader) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=')),
);
const { t, v1 } = parts;
if (!t || !v1) return false;
// Reject replays older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${t}.${rawBody}`)
.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
} catch {
return false;
}
}
// ── Nodukan API client ─────────────────────────────────────────────────────
async function nodukan(method, path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
return res.json();
}
// ── Reply logic ────────────────────────────────────────────────────────────
async function handleNewChat(chatEvent) {
const { id: chatId, buyer, lastMessage } = chatEvent;
// Only reply to messages that haven't been replied to yet
if (lastMessage.senderType !== 'buyer') return;
console.log(`New chat from ${buyer.name}: "${lastMessage.content}"`);
// Simple keyword-based routing
const text = lastMessage.content.toLowerCase();
let reply;
if (text.includes('price') || text.includes('cost') || text.includes('how much')) {
reply = `Hi ${buyer.name}! 👋 Here's my full product catalog with prices: https://ndk.in/catalog. Let me know if you have questions!`;
} else if (text.includes('free') || text.includes('template') || text.includes('notion')) {
reply = `Hey ${buyer.name}! You can grab my free Notion template here: https://ndk.in/notion-template 🎁`;
} else if (text.includes('collab') || text.includes('collaborate') || text.includes('partnership')) {
reply = `Hi ${buyer.name}! I love collaboration ideas. Please fill out this quick form and I'll get back to you: https://ndk.in/collab-form`;
} else {
// Generic welcome reply
reply = `Hi ${buyer.name}! 👋 Thanks for reaching out! I'll get back to you shortly. In the meantime, browse my store: https://ndk.in/store`;
}
const result = await nodukan('POST', `/chats/${chatId}/reply`, { content: reply });
if (result.error) {
console.error('Reply failed:', result.error.message);
} else {
console.log(`Replied to ${buyer.name}: "${reply.substring(0, 60)}..."`);
}
}
// ── Webhook endpoint ───────────────────────────────────────────────────────
// IMPORTANT: Use express.raw() to preserve raw body for HMAC verification
app.post(
'/webhooks/nodukan',
express.raw({ type: 'application/json' }),
async (req, res) => {
const rawBody = req.body.toString('utf8');
const sig = req.headers['x-nodukan-signature'];
if (!verifySignature(rawBody, sig)) {
console.warn('Invalid webhook signature — rejecting');
return res.status(401).json({ error: 'Invalid signature' });
}
// Respond immediately — don't hold the connection open
res.status(200).json({ received: true });
// Process asynchronously
const event = JSON.parse(rawBody);
console.log(`Event received: ${event.event} [${event.id}]`);
try {
switch (event.event) {
case 'chat.new':
await handleNewChat(event.data);
break;
case 'chat.message.received':
// Only handle new buyer messages in existing chats
if (event.data.senderType === 'buyer') {
await handleNewChat({ id: event.data.chatId, buyer: event.data.buyer, lastMessage: event.data });
}
break;
default:
console.log(`Unhandled event type: ${event.event}`);
}
} catch (err) {
console.error('Error processing event:', err);
}
},
);
app.listen(PORT, () => console.log(`Bot listening on http://localhost:${PORT}`));
Step 3 — Expose localhost with ngrok#
ngrok http 3000
# Note the HTTPS URL, e.g.: https://abc123.ngrok.io
Step 4 — Register the webhook#
curl -X POST https://api.nodukan.com/v1/webhooks \
-H "Authorization: Bearer NDK_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/nodukan",
"events": ["chat.new", "chat.message.received"],
"description": "Auto-DM bot"
}'
Copy the signingSecret from the response into your .env:
NODUKAN_WEBHOOK_SECRET=whsec_the_secret_you_just_received
Restart your server. Then trigger a test delivery:
curl -X POST https://api.nodukan.com/v1/webhooks/wh_WEBHOOK_ID/test \
-H "Authorization: Bearer NDK_live_YOUR_KEY"
You should see Event received: lead.captured [evt_test_...] in your server logs.
Step 5 — Test end to end#
Go to your Nodukan storefront or Instagram page and send yourself a message. Within seconds your bot should reply automatically.
Production considerations#
Idempotency — Add a deduplication layer using the event id. Webhook events can be delivered more than once:
import { redis } from './redis.js'; // ioredis or upstash
async function handleNewChat(event) {
const key = `evt:processed:${event.id}`;
if (await redis.get(key)) {
console.log(`Duplicate event ${event.id} — skipping`);
return;
}
await redis.set(key, '1', 'EX', 86400);
// ... process event
}
Queue async work — For anything slower than 1s (LLM calls, CRM writes), use a queue (BullMQ, SQS) so the webhook endpoint always responds in < 500ms.
Error monitoring — Add Sentry or similar so you catch failed replies:
import * as Sentry from '@sentry/node';
// Wrap handlers with try/catch that reports to Sentry
What's next?#
- Lead Magnet Funnel guide — sync captured leads to your CRM
- Webhook Verification guide — deep dive into HMAC in Node.js, Python, PHP, and Go
- Chats API reference — full endpoint documentation
Last updated