NodukanNodukan/ Developers
Guides

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:

  1. Receive a signed webhook event
  2. Look up the buyer context
  3. 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, and webhooks:write scopes
  • 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?#

Was this helpful?
Edit on GitHub

Last updated