· 4 min · TextMeFlow Team

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 401 specifically 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