My WhatsApp bot not replying problem had almost nothing to do with GPT-5
If your WhatsApp bot stops replying after the demo, the problem usually is not prompt quality. It’s usually webhook design. Meta wants a verified public HTTPS endpoint. Replies have to respect the 24-hour service windo
If your WhatsApp bot stops replying after the demo, the problem usually is not prompt quality.
It’s usually webhook design.
Meta wants a verified public HTTPS endpoint. Replies have to respect the 24-hour service window. Media comes in as IDs, not files. And if you do slow AI work inside the webhook request, you’re setting yourself up for random silence.
I learned this the annoying way.
The bot looked great in testing.
Then real users showed up.
Some got replies. Some got nothing. One person sent an image and got ghosted. Another got duplicate replies. Someone came back the next morning and the bot acted like the conversation didn’t exist.
My first instinct was the same one most AI builders have:
- maybe GPT-5 was too slow
- maybe Claude timed out
- maybe the prompt needed cleanup
- maybe n8n was routing image messages into the wrong branch
Reasonable guesses.
Mostly wrong.
The real issue was this:
WhatsApp bots are not mainly an AI problem. They are a workflow reliability problem.
Once I accepted that, the architecture got a lot better.
The demo lied to me
A demo path for a WhatsApp bot is almost always too clean.
You send a short text.
The webhook receives it.
Your app calls the WhatsApp Cloud API.
The bot replies.
Everyone feels smart.
That flow hides the exact things that break in production:
- slow webhook handling
- duplicate deliveries
- media fetch steps
- expired conversation windows
- retry loops
- split error reporting between send responses and webhook events
Text-only demos are a magic trick.
They remove every hard part, then convince you the architecture is fine.
Mine was “fine” too.
Right until users started sending images, PDFs, voice notes, and follow-up messages hours later.
Why the bot was not replying even though the code looked fine
Meta is strict about webhook setup, and honestly it should be.
For WhatsApp Cloud API, your webhook has to:
- verify via
GET - accept
POSTevents on a public HTTPS endpoint with valid TLS
If verification is wrong, Meta does not sort of send webhooks.
It sends nothing.
The verification request looks like this:
GET <CALLBACK_URL>?hub.mode=subscribe&hub.challenge=<HUB_CHALLENGE>&hub.verify_token=<HUB_VERIFY_TOKEN>
Your server has to return:
- HTTP
200 - the exact
hub.challengevalue as the response body
A tiny Express example:
import express from "express";
const app = express();
app.use(express.json());
const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN;
app.get("/webhook", (req, res) => {
const mode = req.query["hub.mode"];
const challenge = req.query["hub.challenge"];
const token = req.query["hub.verify_token"];
if (mode === "subscribe" && token === VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
return res.sendStatus(403);
});
That part bit me once.
But the bigger mistake was what happened after verification.
I was doing too much work inside the webhook request itself.
A message would come in, then my flow would:
- parse the payload
- fetch CRM context
- call an LLM
- maybe load conversation history
- maybe fetch media
- write to Airtable
- then finally return
That’s fine for a toy bot.
It’s bad for production.
The rule I trust now is simple:
ack fast, do the real work later.
The architecture mistake: running AI in the webhook path
If your webhook waits on GPT-5, Claude, OCR, HubSpot, Airtable, or Google Sheets, you’ve turned a message receipt endpoint into a distributed systems bottleneck.
That’s why bots feel random in production.
The fix is boring and effective:
- receive webhook
- validate and normalize payload
- store event
- return
200immediately - process the rest in a queue/worker
A simple shape looks like this:
WhatsApp webhook -> verify + parse -> persist event -> return 200
-> enqueue job
Worker -> fetch media -> run AI -> write CRM/logs -> send reply
If you want a dead-simple local version, even a database table plus a polling worker is better than blocking the webhook on LLM latency.
The “message sent successfully” bug that was not actually success
This one wasted a stupid amount of time.
I would send a reply through the Graph API and get a normal success response.
So I assumed the message was on its way.
That assumption was wrong.
With WhatsApp Cloud API, errors can show up:
- synchronously in the API response
- asynchronously via webhook status/error events
- or both
So “send succeeded” is not one thing.
It’s at least two things:
- the immediate Graph API response
- the later delivery or error webhook
If you only log the first one, your dashboard can look healthy while users are seeing nothing.
That was exactly my bug.
I had observability for requests I initiated.
I did not have observability for delivery events Meta sent back later.
Once I treated outbound messaging as an event stream instead of a single API call, debugging got much easier.
Add a normalization layer before business logic
Raw WhatsApp payloads are consistent, but they’re not fun to route directly into app logic.
You usually care about fields buried in a structure like:
object=whatsapp_business_accountentry[].changes[].value.messages[]- fields like
from,id,timestamp,type,text.body
Instead of wiring that raw shape directly into n8n, Make, or custom handlers, I normalize it first.
Example:
function normalizeWhatsAppMessage(payload) {
const change = payload.entry?.[0]?.changes?.[0]?.value;
const msg = change?.messages?.[0];
if (!msg) return null;
return {
provider: "whatsapp",
messageId: msg.id,
from: msg.from,
timestamp: Number(msg.timestamp),
type: msg.type,
text: msg.text?.body ?? null,
imageId: msg.image?.id ?? null,
audioId: msg.audio?.id ?? null,
documentId: msg.document?.id ?? null
};
}
That one layer made retries, deduping, and media handling much less chaotic.
Media is where the fake-simple bot falls apart
Text messages flatter your architecture.
Media messages expose it.
The first time a user sends an image, PDF, or audio clip, your “simple chatbot” becomes a real integration project.
Incoming media webhooks do not include the file.
They include a media ID.
The real processing path is:
- receive the webhook
- extract the media ID
- call
GET /MEDIA_ID - get a downloadable URL
- fetch the file
- send it to OCR, transcription, or your LLM pipeline
That’s the production path.
A lot of demos skip it because they only test text.
Pseudo-code:
async function fetchMediaUrl(mediaId, accessToken) {
const res = await fetch(`https://graph.facebook.com/v23.0/${mediaId}`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!res.ok) {
throw new Error(`Failed to fetch media metadata: ${res.status}`);
}
return res.json();
}
And yes, there’s a timer here too.
If you delay media retrieval, your “supports images” feature gets fragile fast.
I had one flow where image analysis sat behind lower-priority jobs.
It looked elegant on paper.
In practice, it was a reliability bug.
The fix was simple:
fetch media immediately, store what you need, then do slower AI work later.
The 24-hour rule is not product trivia
This is where people say, “WhatsApp randomly stopped replying.”
Usually, no.
Your message policy changed underneath you.
WhatsApp has a 24-hour customer service window.
It starts when a user messages or calls you, and each new user message resets the timer.
Inside that window, you can send free-form replies.
Outside it, you need an approved template.
So yes, your bot can look perfect in testing and then “break” in production once real conversations age out.
That’s not an edge case.
That is runtime behavior.
| Pattern | What happens in production |
|---|---|
| Text-only messages | Usually look reliable in testing because there is no media fetch or long processing chain |
| Media messages via media ID fetch | Break if you assume the file is in the webhook or you wait too long to retrieve it |
| Template vs service messages | Free-form replies stop working outside the 24-hour window unless you use an approved template |
Once I started treating policy rules as part of the system design, the bot got much easier to reason about.
The resend loop from hell
Bad retries create chaos fast.
Meta documents throttling errors like 130429 for rate limits.
The important part is that you should handle errors based on the code and details fields, not just HTTP status.
A dumb retry loop can turn one temporary issue into three new ones:
- duplicate customer replies
- duplicate LLM calls to GPT-5 or Claude
- duplicate writes to HubSpot, Notion, Salesforce, or Airtable
I hit exactly this.
One inbound event got processed twice.
My worker retried an outbound send too aggressively.
The user got duplicate messages.
My logs looked like a conspiracy board.
The reliability stack I wish I had from day one
This is the stack I trust now:
-
Idempotency keys on inbound events using the WhatsApp message ID like
wamid... - Fast webhook ACK before any long-running work
- Queue + worker for LLM calls, CRM writes, and media processing
- Backoff with jitter for outbound retries
- Separate monitoring for Graph API responses and webhook delivery/error events
- Conversation-window checks before sending free-form replies
A simple dedupe example with Redis:
async function shouldProcessMessage(redis, messageId) {
const key = `wa:msg:${messageId}`;
const wasSet = await redis.set(key, "1", {
NX: true,
EX: 60 * 60 * 24
});
return wasSet === "OK";
}
Basic backoff with jitter:
function retryDelay(attempt) {
const base = Math.min(1000 * 2 ** attempt, 30000);
const jitter = Math.floor(Math.random() * 500);
return base + jitter;
}
That stack is less glamorous than prompt engineering.
It is also the reason the bot started behaving like a product instead of a hackathon demo.
Do you always need a queue and workers?
No.
If your bot only sends short text replies and does almost nothing else, a synchronous path can be fine for a while.
Not every side project needs Kafka, Redis Streams, BullMQ, SQS, or a whole reliability war room.
But the moment you add any of these, the simple path starts lying to you:
- GPT-5, Claude, Qwen, or Llama inference
- OCR or transcription for images and audio
- HubSpot or Salesforce writes
- Google Sheets, Airtable, or Notion logging
- multi-step flows in n8n or Make
That’s the threshold.
Once the webhook is waiting on external systems, you are not building a chatbot anymore.
You are building a distributed workflow with a chat frontend.
That mental model is much more useful.
The production architecture I’d build first now
If I had to rebuild the whole thing, I’d start here:
| Webhook handling pattern | Reality check |
|---|---|
| Synchronous inline processing | Fine for demos, fragile once you add AI, media, or CRM calls |
| Fast ACK + queue/worker | Best default for production because it keeps webhook latency low |
| Idempotency support | Non-negotiable if you want to survive duplicate events and retries |
And the full flow:
WhatsApp webhook -> verify + parse -> store event -> return 200 fast
-> queue job
Queue worker -> fetch media if needed -> run AI -> write CRM/logs -> send reply
Monitor -> Graph API send response + WhatsApp webhook status/error events
That architecture is not fancy.
That’s why I like it.
It matches how Meta actually delivers messages.
It handles the fact that media retrieval is a second step.
It survives slow LLM calls.
And it stops pretending a successful API response means the user definitely got the reply.
One practical note if you are calling LLMs from these workflows
Once I moved AI work off the webhook path, the next bottleneck was cost predictability.
That matters a lot if your bot is doing:
- image analysis
- long conversation context
- retries
- multi-step agent flows in n8n, Make, or custom workers
Per-token billing gets annoying fast when you’re debugging production reliability and also watching usage meters.
That’s one reason I like what Standard Compute is doing.
It gives you an OpenAI-compatible API endpoint, but with flat monthly pricing instead of per-token billing. If you’re building bots or automations that call GPT-5.4, Claude Opus 4.6, or Grok 4.20 behind the scenes, predictable cost is a real operational advantage.
Especially when your actual problem is workflow reliability, not whether one extra retry cost 40 cents.
Final takeaway
I started this build obsessed with prompts.
I ended it obsessed with:
- webhook verification
- timeouts
- idempotency
- media retrieval
- retries
- delivery events
- the 24-hour conversation window
That felt disappointing for about five minutes.
Then the bot started replying reliably.
And that turned out to be much more useful than one more clever prompt.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.