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_secret2. 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 wherekey.fromMeis 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 like233244000000, 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.