Deploying a WhatsApp bot means working with Meta's WhatsApp Cloud API, which is webhook-driven: WhatsApp POSTs incoming messages to a public HTTPS endpoint you control, and you reply by calling the Graph API. The deployment is fundamentally a small, always-on web service with a verified webhook. This guide covers the official Cloud API path, which is the supportable, terms-compliant way to build a WhatsApp bot.
How the Cloud API works
There are two flows:
- 1Inbound: a user messages your WhatsApp business number; WhatsApp delivers that message to your webhook URL via HTTP POST.
- 2Outbound: your bot sends a message by POSTing to the Graph API endpoint for your phone-number ID, using a permanent access token.
Your deployed service must expose a publicly reachable HTTPS endpoint with a valid certificate, because Meta will not deliver to plain HTTP.
Webhook setup and verification
When you register the webhook, Meta sends a GET request with a verification challenge. Your endpoint must echo back the hub.challenge value if the hub.verify_token matches a secret you chose:
import express from 'express';
const app = express();
app.use(express.json());
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
res.sendStatus(403);
});Get this wrong and Meta won't activate your webhook. The verify token is a value you invent and configure in both your environment and the Meta app dashboard.
Receiving and replying
Incoming messages arrive as a nested JSON structure. Acknowledge fast with a 200, then process:
app.post('/webhook', async (req, res) => {
res.sendStatus(200); // ack immediately
const entry = req.body.entry?.[0]?.changes?.[0]?.value;
const msg = entry?.messages?.[0];
if (!msg) return;
await sendMessage(msg.from, `You said: ${msg.text?.body}`);
});
async function sendMessage(to, body) {
await fetch(`https://graph.facebook.com/v21.0/${process.env.PHONE_NUMBER_ID}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'text',
text: { body },
}),
});
}Reply quickly with a 200 so Meta doesn't retry the delivery; do the actual work asynchronously after acknowledging.
Validating payload signatures
Meta signs webhook payloads with an X-Hub-Signature-256 header (HMAC-SHA256 using your app secret). Verify it so nobody can forge messages to your endpoint:
import crypto from 'crypto';
function verifySignature(req, rawBody) {
const sig = req.get('X-Hub-Signature-256') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.APP_SECRET)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Use a raw-body parser to compute the HMAC over the exact bytes Meta sent.
The 24-hour window and templates
A detail that surprises new WhatsApp developers: you can freely reply to a user within 24 hours of their last message. Outside that window, you can only send pre-approved message templates. Design your bot around this, and store the last-interaction timestamp per user (a good reason to attach a database).
Dockerfile and deploy
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]On PandaStack:
- 1Connect the GitHub repo as a container app.
- 2Add
VERIFY_TOKEN,WHATSAPP_TOKEN,PHONE_NUMBER_ID, andAPP_SECRETas environment variables. - 3PandaStack builds with rootless BuildKit and deploys via Helm with automatic SSL on the assigned domain.
- 4Set your webhook URL in the Meta app dashboard to
https://your-app.pandastack.app/webhookand complete verification. - 5Optionally attach a managed PostgreSQL database to track conversation state and the 24-hour window;
DATABASE_URLis injected automatically.
| Requirement | Detail |
|---|---|
| Public HTTPS | Required; auto SSL covers it |
| Webhook GET verify | Echo hub.challenge |
| Signature check | HMAC-SHA256 with app secret |
| Outbound auth | Bearer token to Graph API |
| 24-hour rule | Templates required outside window |
Keep it warm
Like any webhook-driven bot, cold starts hurt responsiveness. The free tier's scale-to-zero is fine for a prototype, but for a bot people actually message, run a warm instance so replies feel instant. WhatsApp doesn't have Slack's hard three-second deadline, but users expect quick replies.
Verifying
Message your business number and watch the live logs for the inbound payload and your outbound call. Confirm the signature check passes and the reply lands in WhatsApp.
References
- WhatsApp Cloud API getting started: https://developers.facebook.com/docs/whatsapp/cloud-api/get-started
- Webhook setup and verification: https://developers.facebook.com/docs/graph-api/webhooks/getting-started
- Sending messages: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages
- Webhook payload signature validation: https://developers.facebook.com/docs/graph-api/webhooks/getting-started#validate-payloads
- Messaging window and templates: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates
A WhatsApp bot is a verified webhook plus outbound Graph API calls, best run as a warm, always-on service. Deploy yours with managed secrets and an optional database on PandaStack's free tier: https://dashboard.pandastack.io