WhatsApp Webhook Retries and Idempotency — Why You Need an Event-Id Check
If you receive incoming WhatsApp messages through a textmeflow webhook, your
endpoint will eventually see the same event more than once. That's not a bug
— it's how reliable webhook delivery works. If your endpoint doesn't account
for it, you'll end up double-sending a reply, double-charging an order, or
logging duplicate support tickets. This article covers the retry policy and
the one header that fixes all of it: X-TextMeFlow-Event-Id.
Why retries happen at all
A webhook delivery is a POST request over the public internet to a server you control. Between textmeflow and your endpoint there's DNS, TLS, your load balancer, your framework's middleware stack, and your application code — any of which can fail or time out for a few seconds without anything being wrong with your integration. If textmeflow gave up after the first attempt, a blip on your side would mean a lost WhatsApp message. So it retries.
The practical consequence: your endpoint must assume it can receive the same webhook delivery more than once, and must not treat that as an error.
The retry schedule
Every delivery attempt is judged purely on the HTTP response:
- 2xx within 10 seconds — delivered, no retry.
- 408, 429, 5xx, or a timeout/network error — retryable. textmeflow
retries up to 5 times with backoff
1m, 5m, 30m, 2h, 6h, so up to 6 total attempts (the original plus 5 retries) spread over roughly 8-9 hours. - Any other 4xx — treated as failed, no retry. A
401specifically almost always means your HMAC secret no longer matches what's configured — fix it on your side rather than expecting a retry to help.
After the 6th attempt, the delivery is marked failed and you can inspect
every attempt's outcome in /admin/webhook. Respond fast and with a 2xx as
soon as you've durably queued the event — don't do slow processing (API
calls, heavy database writes) inside the request handler itself, since a
slow 2xx that misses the 10-second window still counts as a timeout.
The header that makes retries safe
Every webhook payload carries two different identifiers, and conflating them is the most common mistake:
X-TextMeFlow-Event-Id— a UUID identifying the message. It stays identical across every retry of that same delivery.X-TextMeFlow-Delivery— a UUID identifying the delivery attempt. It's different on every retry, which makes it useful for attempt-level log grep, but useless for deduplication.
To make your endpoint idempotent, store X-TextMeFlow-Event-Id as the
primary key (or a unique-indexed column) wherever you persist incoming
messages, and check it before acting on a new one:
$eventId = $request->header('X-TextMeFlow-Event-Id');
if (InboundMessage::where('event_id', $eventId)->exists()) {
return response('', 200); // already processed, ack and stop
}
InboundMessage::create([
'event_id' => $eventId,
'payload' => $request->getContent(),
]);
// ...queue the actual processing, then return 2xx
return response('', 200);
The same pattern works in any language: a unique constraint on the event id plus an early return. No distributed lock, no deduplication window to tune — the id is stable for the life of that delivery, so a plain uniqueness check is enough.
Combine it with signature verification
Idempotency and signature checking solve different problems and you need
both. Verifying X-TextMeFlow-Signature (HMAC-SHA256 over the raw body,
compared constant-time) confirms the request actually came from textmeflow;
checking X-TextMeFlow-Event-Id confirms you haven't already processed this
particular message. See
Verifying WhatsApp webhook signatures
for the signature side, and the full header and retry reference in the
webhooks documentation.
What this prevents in practice
- A customer getting the same auto-reply twice because your handler ran on both the original attempt and a retry triggered by a slow response.
- An order status flipping back and forth if two attempts race each other — dedupe on event id before you touch any mutable state.
- Noisy alerting: if your error tracker fires on every retryable 5xx, you'll get paged for the exact failures the retry schedule is designed to absorb on its own.
Get this right once at the top of your webhook handler and every integration you build on top of it — order confirmations, support ticket creation, CRM sync — inherits the safety for free.
Want to see the retry behavior yourself? Create a free textmeflow account,
configure a webhook endpoint, and use the one-click test fire from
/admin/webhook to watch the headers land.
Zelf WhatsApp-berichten versturen via API?
Gratis voor altijd tot 50 berichten/maand. QR scannen en binnen 5 minuten verstuur je je eerste bericht.
Gratis voor altijd