跳到正文
DEV Community· Lars Winstand·· 2 小時前AI 評分57

WhatsApp 機械人無法回覆,問題幾乎與 GPT-5 無關

My WhatsApp bot not replying problem had almost nothing to do with GPT-5

AI 導讀

Lars Winstand 發現,自己的 WhatsApp 機械人停止回覆,問題幾乎不在 GPT-5,而在 webhook 設計及工作流程可靠性。文章指出,WhatsApp webhook 需要經驗證的公開 HTTPS endpoint,媒體訊息要按 media ID 另行擷取,而自由格式回覆受 24 小時客戶服務時段限制。

正文

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:

  1. verify via GET
  2. accept POST events 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.challenge value 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 200 immediately
  • 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_account
  • entry[].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:

  1. receive the webhook
  2. extract the media ID
  3. call GET /MEDIA_ID
  4. get a downloadable URL
  5. fetch the file
  6. 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:

  1. Idempotency keys on inbound events using the WhatsApp message ID like wamid...
  2. Fast webhook ACK before any long-running work
  3. Queue + worker for LLM calls, CRM writes, and media processing
  4. Backoff with jitter for outbound retries
  5. Separate monitoring for Graph API responses and webhook delivery/error events
  6. 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.

來源:DEV Community · dev.to