EnoSend

Auto-reply to WhatsApp messages with Node.js and EnoSend

Auto-reply to WhatsApp messages with Node.js and EnoSend

Sending messages is the easy half. The half that actually keeps customers is answering them, fast, even when you are asleep.

Here is a small Node.js server that listens for incoming WhatsApp messages through EnoSend, checks they are genuine, and fires back a reply. It is the skeleton for an auto-responder, a support bot, or later an AI agent.

What you need

  • An EnoSend account with a connected WhatsApp number. Plans start at GHS 55/month, no per-message fees, 3-day trial.

  • Your API key (wa_live_...), your instance id, and your webhook signing secret from the dashboard.

  • In the dashboard, point your webhook at https://your-server.com/webhooks/enosend.

1. Environment

ENOSEND_API_KEY=wa_live_YOUR_KEY
ENOSEND_INSTANCE_ID=your_instance_id
ENOSEND_WEBHOOK_SECRET=your_webhook_secret

2. The server

The one trick that trips people up: to verify the signature you need the raw request body, so do not let a JSON parser touch it first. We read it as a buffer, verify, then parse ourselves.

const express = require("express");
const crypto = require("crypto");

const app = express();

const { ENOSEND_API_KEY, ENOSEND_INSTANCE_ID, ENOSEND_WEBHOOK_SECRET } = process.env;

const BASE = "https://api.enosend.com/api/wa/v1";

function safeEqual(a, b) {

  const ba = Buffer.from(a), bb = Buffer.from(b);

  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);

}

// express.raw, NOT express.json, so we keep the exact bytes for the signature.

app.post("/webhooks/enosend", express.raw({ type: "*/*" }), async (req, res) => {

  const raw = req.body; // Buffer

  const header = req.get("X-AddinyHost-Signature") || "";

  const expected = "sha256=" + crypto.createHmac("sha256", ENOSEND_WEBHOOK_SECRET)

    .update(raw).digest("hex");

  if (!safeEqual(expected, header)) {

    return res.status(401).send("bad signature");

  }

  const payload = JSON.parse(raw.toString("utf8"));

  // { event, instance_id, data }

  if (payload.event === "MESSAGES_UPSERT") {

    const key = payload.data?.key || {};

    if (!key.fromMe) {                        // ignore messages we sent

      const from = (key.remoteJid || "").split("@")[0];   // 233244000000

      const m = payload.data?.message || {};

      const text = m.conversation || m.extendedTextMessage?.text || "";

      await reply(from, Thanks, we got your message: "${text}". Someone will reply shortly.);

    }

  }

  res.json({ ok: true });

});

async function reply(number, text) {

  await fetch${BASE}/instances/${ENOSEND_INSTANCE_ID}/messages/text, {

    method: "POST",

    headers: {

      "Authorization": Bearer ${ENOSEND_API_KEY},

      "Content-Type": "application/json",

    },

    body: JSON.stringify({ number, text }),

  });

}

app.listen(3000, () => console.log("listening on :3000"));

3. What is happening

  • Every webhook is signed with HMAC-SHA256. We rebuild that signature over the raw body and compare it in constant time with safeEqual, so a fake event gets a 401.

  • We only act on MESSAGES_UPSERT, which is an incoming or newly seen message. We skip anything where key.fromMe is true, otherwise the bot would reply to its own replies forever.

  • The sender's number comes from remoteJid. We strip the part after @ to get plain digits like 233244000000, which is exactly the format the send endpoint wants.

Where this goes next

Right now it echoes the message back. Swap that one line for a lookup ("your order is on the way"), an FAQ answer, or a call to an AI model that reads the text and replies in the customer's own words. The plumbing stays the same. That is the point.

Bottom line

Receive, verify, reply. Under 40 lines of Node. Verify the signature, skip your own messages, and you have the foundation for anything from a simple auto-reply to a full WhatsApp assistant.