WhatsApp Webhook Reliability: A Production Acceptance Test You Can Actually Run
Decision in one sentence: acknowledge a WhatsApp webhook only after its authenticated request has been durably recorded, then process it asynchronously with an idempotent event ledger; test that contract with signed fixtures, duplicate deliveries, reordered statuses, queue failures, and replay before connecting customer workflows.
This is a production-readiness guide, not a claim that a particular queue, framework, or hosting provider is inherently safe. The acceptance matrix and harness below are original test assets. They make a specific promise testable: a valid delivery is never silently lost, an invalid delivery cannot trigger business actions, and a retry cannot create a second side effect.
What this article adds beyond a webhook setup tutorial
Most setup guides stop after the endpoint verifies, the provider sends a test event, and a sample route returns HTTP 200. That proves reachability. It does not prove that your database committed the event before the acknowledgement, that a replay is harmless, or that a status event arriving late cannot regress a conversation. The existing WhatsApp webhook testing guide recommends defining scenarios and checking normal, incomplete, and contradictory inputs. This article turns that broad advice into a repeatable acceptance contract: twelve failure-oriented cases, explicit pass conditions, and a small executable signature/idempotency fixture.
The primary product reference is Meta's WhatsApp Business Developer Hub, which links to the Cloud API, webhook, error-code, rate-limit, and testing materials. A readable copy of Meta's WhatsApp webhook overview describes inbound messages and status notifications, endpoint setup, subscribed fields, payload limits, and retry behavior. It says failed deliveries may be retried with decreasing frequency for up to seven days. The direct Meta documentation endpoint returned HTTP 429 during this research pass, so treat the mirror as a navigation aid and verify changing retry details against Meta's live documentation before setting retention or alert thresholds. A generic Graph webhook retry statement is not automatically a WhatsApp Cloud API guarantee: product, event, and documentation scope matter.
The operating contract: authenticate, persist, acknowledge, process
Separate the webhook's synchronous responsibility from the application's business work. The synchronous route should read the exact request bytes, verify the provider signature, apply basic size and schema limits, write a durable inbox record or queue message, and only then return success. A worker can perform slower work—updating a CRM, sending a reply, invoking an AI model, or notifying an agent—after that commit. This is the practical consequence of the security guidance in the OWASP Webhook Security Cheat Sheet: validate signatures against the raw body, do not treat a valid signature as replay protection, persist identifiers, make side effects idempotent, and do not assume events arrive in order.
Fact: the WhatsApp product uses a GET verification handshake and POST notifications. The verification token used during endpoint setup is not a substitute for validating the signature on each POST. Inference: if a service sends success before durable storage, it has created a gap in which the provider can stop retrying while the application has no recoverable copy. Recommendation: define the durable commit point explicitly. It can be a database transaction, a queue with confirmed persistence, or a transactional inbox; an in-memory promise, log line, or fire-and-forget enqueue is not that commit point.
The key distinction is between delivery and work. A delivery can be accepted and stored even when a downstream CRM is unavailable. Conversely, a request that has not been stored must not be acknowledged merely because the route parsed JSON successfully. This is why “return 200 quickly” is incomplete advice. Return success quickly after a durable handoff, not before it.
flowchart LR
A[Provider POST] --> B[Read raw bytes]
B --> C{Signature valid?}
C -- no --> D[Reject; no side effect]
C -- yes --> E[Validate envelope and limits]
E --> F{Durable inbox commit?}
F -- no --> G[Return retryable failure]
F -- yes --> H[Return success]
F --> I[Async worker]
I --> J{Event already applied?}
J -- yes --> K[Record duplicate; no repeated effect]
J -- no --> L[Apply guarded business transition]
L --> M[Record outcome and retry/dead-letter if needed]
A twelve-case acceptance matrix
The cases below are a test plan, not measured production outcomes. Each row names an input, the observable pass condition, and the failure that the case is intended to expose. Run them against a staging endpoint with a test business number and synthetic records; never replay a real customer's message into a production workflow simply to test idempotency. Save the request fixture, expected response, stored inbox row, worker result, and any outbound message count as the evidence packet.
| # | Scenario | Required pass condition | Failure caught |
|---|---|---|---|
| 1 | Valid signed message | One durable inbox record before success response | Ack-before-commit loss |
| 2 | One byte changed after signing | Rejected; zero business effects | Parsing or signing wrong bytes |
| 3 | Missing or malformed signature | Rejected and security event logged without body secrets | Unauthenticated action |
| 4 | Same signed delivery twice | Both requests safely acknowledged after one logical effect | Duplicate side effect |
| 5 | Two status updates arrive out of order | Ledger retains both; current state does not regress | Arrival-order overwrite |
| 6 | CRM is slow after inbox commit | Webhook response remains bounded; worker retries independently | Provider timeout coupled to CRM |
| 7 | Queue/database commit fails | No success acknowledgement; alert and retry path visible | Silent event loss |
| 8 | Worker crashes after commit, before effect | Lease/retry recovers work without duplicate effect | Lost or repeated worker job |
| 9 | Worker crashes after effect, before result record | Idempotency key or transactional boundary prevents second effect | Ambiguous completion |
| 10 | Oversized or malformed payload | Controlled rejection; no unbounded allocation or partial action | Resource exhaustion / parser drift |
| 11 | Secret rotation overlap | Current and approved previous secret behavior is explicit and time-bounded | Outage or indefinite stale secret |
| 12 | Unknown event field or new event type | Stored safely or rejected by documented policy; no guessed action | Schema evolution becoming a command |
The matrix intentionally distinguishes a duplicate HTTP delivery from a duplicate business event. A retried POST can carry the same logical event, but the same message may also legitimately receive multiple status updates over its lifecycle. Therefore, deduplicating on message_id alone can discard a later read or failed update. Preserve the event record and derive state with a version-aware rule rather than overwriting a row in arrival order. For inbound message processing, the WhatsApp message identifier is useful as a domain key; for status notifications, include the event kind and relevant status timestamp or another documented version discriminator. Validate the exact fields against the current Meta schema, not an assumption copied from an old SDK.
The scenario count is simply the number of rows in this author-created matrix, dated 25 October 2026. It is not a failure rate, benchmark, or claim about how often these conditions occur. No production traffic was sampled. Tests that depend on account configuration, provider-side retry timing, media retrieval, or a real conversation are excluded from the local harness and require a controlled staging exercise.
The smallest useful executable fixture
This Node.js example does not implement a web server or claim to be Meta's official SDK. It demonstrates two properties that can be unit-tested without sending a message: HMAC verification over the original bytes and an idempotent in-memory ledger. Replace the ledger with a transactional database in production. The signature header format and secret must be confirmed against the current provider documentation for your account and API version.
// webhook-fixture.mjs — node webhook-fixture.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
function signature(rawBody, secret) {
return `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
}
function verify(rawBody, supplied, secret) {
if (typeof supplied !== 'string' || !supplied.startsWith('sha256=')) return false;
const expected = Buffer.from(signature(rawBody, secret));
const actual = Buffer.from(supplied);
return expected.length === actual.length && timingSafeEqual(expected, actual);
}
const secret = 'fixture-only-secret';
const raw = Buffer.from('{"event":"message","id":"wamid.fixture-1"}');
const ledger = new Map();
function accept(rawBody, sig) {
if (!verify(rawBody, sig, secret)) return { status: 401, effect: false };
const event = JSON.parse(rawBody.toString('utf8'));
const key = `${event.event}:${event.id}`;
if (ledger.has(key)) return { status: 200, effect: false, duplicate: true };
ledger.set(key, { rawBody: rawBody.toString('base64'), state: 'queued' });
return { status: 200, effect: true, duplicate: false };
}
const good = signature(raw, secret);
const tampered = Buffer.from(raw);
tampered[10] ^= 1;
console.log({ first: accept(raw, good), replay: accept(raw, good), tampered: accept(tampered, good) });
Expected shape: the first valid fixture is queued once; the repeated fixture is acknowledged as a duplicate without another effect; the modified bytes fail verification. This harness does not prove database durability, queue behavior, timing bounds, provider delivery semantics, or correctness of the chosen event key. Add assertions and replace Map with a durable test database before treating it as a system acceptance test. Keeping the code deliberately small is useful: it isolates the raw-byte and duplicate invariants instead of hiding them inside an application framework.
A replay-safe event identity without losing status changes
Idempotency is not “ignore every request with a familiar message ID.” It is a contract between the delivery layer and a specific side effect. Store an immutable delivery record first: internal row ID, provider, phone-number/business account identifier, receipt time, signature-verification result, event type, provider event/message ID when present, raw-body hash, schema version, and processing state. Restrict access to raw payloads because message content can contain personal data. A separate projection can maintain the latest known message state while the event ledger preserves the history used to explain it.
For a message status, a safe derived-state function should compare recognized transitions instead of trusting arrival order. A delivered notification received after read must not downgrade the displayed status to delivered. A failed status may need to be represented as an error alongside delivery history rather than forced into one universal linear ranking. Keep the original event and the projection decision. If an event is unknown, record it and route it for inspection; do not infer that it authorizes a reply, refund, lead qualification, or deletion.
The Stripe idempotency-key documentation is a useful design analogy for preventing duplicated API side effects, but it governs Stripe requests, not WhatsApp webhook delivery. Use it to understand the pattern—stable key, bounded result reuse, explicit retry semantics—not as evidence of Meta's guarantees. For a side effect sent to another service, pass your own stable idempotency key if that service supports one. If it does not, create an outbox record and define reconciliation for the ambiguous case where the remote action succeeded but your worker crashed before recording success.
The acknowledgement boundary and the queue outage test
The most important failure case is not “the worker is down”; it is “the request path returned success although no recoverable copy exists.” Exercise that boundary by injecting a database or queue write failure after signature validation. The expected result is a non-success response, a bounded error record that excludes secrets and message text, and a visible alert. Then restore storage and replay the same signed fixture. It should produce exactly one durable inbox item and one business effect. If the first failed attempt somehow created the effect, the replay test should expose the inconsistency rather than conceal it.
If your database transaction and queue are separate systems, a process can crash between the two writes. A transactional outbox or inbox pattern reduces this gap: commit the accepted event and an outbox task in one database transaction, then let a dispatcher publish tasks and mark them sent. The dispatcher itself must tolerate repeating the publish after a crash. The worker must also tolerate a duplicate task. “Exactly once” across independent systems is not a property to assume; design for at-least-once attempts and idempotent effects, then document the transaction boundaries that make the user-visible outcome effectively once.
The OWASP webhook guidance supports validating before processing, persisting identifiers, handling duplicate requests, and accounting for replay. CISA's Secure by Design guidance adds a broader operational frame: security should be built into product defaults and lifecycle decisions, not left entirely to each customer. It is not a WhatsApp protocol specification; it supports the general recommendation to make safe acknowledgement, logging, and secret handling default properties of the integration.
Signature validation is necessary, not sufficient
Read the raw body before JSON parsing changes its representation. JSON can be serialized with different whitespace, property order, or escaping while describing similar data; a signature authenticates bytes, not your parsed object. Compute the expected HMAC using the configured app secret, compare in constant time, and reject malformed encodings. Never log the secret or full signature. Do not accept a request merely because it came from a provider-looking IP address or carries a plausible sender field.
Signature verification does not stop a captured valid request from being replayed. Add event-level deduplication and, where the provider's current contract makes a timestamp available, a bounded freshness check that does not reject legitimate retries after a temporary outage. There is a real trade-off: an aggressive timestamp window can turn a recoverable delayed delivery into data loss, while an unlimited window expands replay exposure. The right policy depends on provider retry rules, clock discipline, and whether old but authentic events can safely be reprocessed. Preserve the durable inbox record so operators can distinguish a stale replay from an unknown event.
Keep the GET verification challenge distinct from POST authentication. During setup, a provider may issue a challenge that your endpoint returns only if a configured verification token matches. That proves endpoint ownership for subscription configuration. It does not authenticate subsequent POST payloads. Conversely, POST signature validation should not rely on a query-string token. Treat these as separate routes with separate tests and separate logs.
Retry windows, duplicate subscribers, and what the docs do not settle
The mirrored WhatsApp webhook overview says failed webhook deliveries may be retried with decreasing frequency for up to seven days and notes that subscribing multiple apps can result in multiple retry streams. The important operational conclusion is to make a delivery safe to repeat and to keep an internal recovery path; the exact schedule, event coverage, and behavior can change. The direct Meta page was rate-limited during this research, so this article does not prescribe retry timing or promise seven days of recoverability. Confirm the current WhatsApp-specific docs, your app subscription, and the behavior observed in your own test account.
Do not confuse provider retries with your worker retries. Once your endpoint has acknowledged a durable inbox item, the provider's retry mechanism is no longer your queue recovery system. Your application needs its own retry policy, max attempts, backoff, dead-letter queue, operator alert, replay command, and retention policy. Conversely, if you do not acknowledge because persistence failed, the provider may retry according to its own schedule. Those are two different loops and need separate metrics.
Repeated notifications can also result from multiple subscribed apps or environments, not only a provider retry. Include the business account, phone number ID, application/environment, and event key in logs and inbox uniqueness constraints. A staging endpoint connected to a production subscription is a configuration hazard. Make environment identity visible in the request dashboard and ensure only the intended app receives the intended fields.
Why “HTTP 200” anecdotes are weak evidence
A Stack Overflow discussion about repeated older WhatsApp Cloud API notifications illustrates a genuine troubleshooting question: a developer described old inbound notifications arriving repeatedly, while commenters discussed successful HTTP responses, cached payloads, and possible flushing of previously undelivered events. It is anecdotal, not a controlled study, and it does not establish a provider-wide behavior. Its value is in suggesting diagnostic branches: inspect event IDs, raw-body hashes, app subscriptions, endpoint history, and durable acknowledgements before concluding the provider is generating new messages.
When a customer says “we got the same message twice,” first distinguish four observations: two HTTP deliveries with the same body; two stored rows representing the same provider event; two different events for one message; or two outbound actions caused by one event. These are not interchangeable. A dashboard should show each layer and its identifiers. If you only look at the CRM, a duplicate side effect may conceal a healthy event ledger; if you only inspect server logs, a single request that created two actions may look like a provider duplicate.
Ask the person reporting the issue for a time range, business phone number, message ID, and whether a second response was sent. Redact message text and recipient numbers in the shared diagnostic packet unless they are essential and access-controlled. Verify provider-side app subscriptions and your own endpoint logs. A practical postmortem compares the payload hash, provider identifier, internal inbox key, worker attempt ID, and outbound action ID. This is a diagnosis method, not proof that any particular report has a provider-side cause.
Ordering and state projection
Arrival time is not necessarily event time. Network retries, queue scheduling, multiple workers, and backfills can make a newer state arrive before an older one. Preserve provider timestamps when documented, record server receipt time separately, and never silently replace one with the other. Use a projection function that is deterministic: given the set of valid events, it computes a current display state while retaining the event ledger.
For example, an inbound message might be accepted, queued, processed, and responded to. Those are application processing states, not necessarily Meta delivery statuses. Keep namespaces distinct. For outbound messages, sent, delivered, read, and failed are provider-facing status concepts; their precise schema and transition semantics must be verified against the API version you use. A state machine should include unknown and conflict states instead of forcing every combination into a happy path.
Never let a late delivered event overwrite a previously observed read event. Do not assume failed can always be sorted numerically below or above another status. Preserve it as an event and define the display rule deliberately. For business actions, make the action gate depend on a specific state and policy—for example, only send a reply once for a given inbound message and conversation rule—not on “the most recently received webhook.”
Security, privacy, and retention controls
Webhook bodies may contain identifiers, message text, media references, and user data. Keep them out of ordinary application logs, analytics events, error trackers, and support tickets by default. Log a correlation ID, event class, signature outcome, payload hash, and internal record ID. If engineers need to inspect a payload, use a short-lived, audited access path with redaction. Define retention by operational need and applicable privacy obligations; do not keep raw content forever simply because storage is cheap.
Use separate secrets per environment and rotate them deliberately. A rotation test should document whether the endpoint accepts one current secret or a short overlap set, how long overlap lasts, how an old secret is revoked, and how an alert detects stale configuration. Never let “accept any of these keys indefinitely” become the emergency plan. Protect secret delivery through a secret manager, limit who can read it, and ensure exception traces cannot include it.
Validate content after authentication. A valid signature means a configured secret signed those bytes; it does not mean every field is safe or every action is authorized. Enforce payload size, expected account and phone number identifiers, known event fields, and safe parsing. Treat URLs in message content as untrusted data. If the event causes an AI agent to take action, pass only the minimum necessary content, preserve the original event reference, and require a separate policy gate for consequential actions. Authentication of the webhook is not consent to perform every downstream action.
The OECD AI policy topic is contextual background for governance of AI-enabled systems, not a webhook transport standard. The McKinsey AI insights collection discusses organizational adoption and operating models, not Meta delivery semantics. They are included only as broader context if your webhook feeds an AI-assisted service; protocol claims in this article rely on Meta-linked documentation and OWASP security guidance.
A staging runbook with explicit evidence
Before the run, record the API version, app and business-account identifiers, subscribed fields, callback URL, deployment commit, secret version identifier (never the secret), queue/database version, and test-number configuration. Use synthetic message content such as acceptance-fixture-001. Set an observation window and an owner who can stop outbound customer actions. Confirm staging cannot reach a production CRM or a live WhatsApp recipient unless the test is explicitly approved.
For each matrix row, capture: input fixture ID, raw-byte hash, signature result, HTTP status and latency, inbox primary key, queue/outbox state, worker attempts, final projection, outbound side-effect count, and cleanup status. Store no full message body in the test report. A pass is not “the route returned 200”; it is the conjunction of the expected acknowledgement, durable record, correct worker outcome, and exactly the permitted number of effects.
Run the same duplicate fixture twice, then restart the worker between attempts. Inject the storage failure before the commit, not merely after a successful request. For the crash-after-effect case, deliberately stop the worker after the remote side effect but before local completion is recorded. That is the uncomfortable boundary where an idempotency key or reconciliation process proves its value. If the remote system offers no idempotency feature, your test should demonstrate how an operator resolves an ambiguous result without blindly resending.
At the end, export a compact evidence packet and compare it with the prior run. A configuration change to the webhook API version, signature middleware, queue, retry policy, or idempotency key is a reason to rerun affected cases. Keep the baseline result and the current result together; do not replace a failed run with only the later green report. A known failure with a documented mitigation is more useful than an undocumented claim of “tested.”
Failure modes and recovery decisions
Success response before persistence. This is a loss hazard. Change the route so the acknowledgement follows the durable commit. If persistence is unavailable, return the retryable failure prescribed by the current provider contract and page the owner; do not invent an HTTP code without checking provider behavior.
Duplicate request creates a duplicate reply. Add a unique inbox key and an idempotency boundary around the outbound action. Then test the crash window after sending but before recording the result. If the messaging API accepts an idempotency mechanism, use it; if not, keep an action ledger and a reconciliation path.
Raw-body signature always fails in production. Check middleware order, body parser configuration, encoding, secret source, header normalization, and whether a proxy changed bytes. Never “fix” the alert by disabling verification. Create a fixture from the exact bytes sent by the test provider and compare hashes at each hop.
Old statuses overwrite newer ones. Separate provider event history from the UI projection. Add deterministic transition rules and a test that delivers events in reverse order. If the source schema lacks enough ordering data, surface a conflict or retain both; do not manufacture a precise current state.
Queue unavailable but endpoint still returns success. Trace the durable commit point. A health endpoint can be green while the specific write path is broken. Inject a failed write and assert non-success, alert, and successful later replay.
Retries continue for days or never arrive. Do not diagnose from assumption. Verify app subscriptions, endpoint response history, provider error codes, and the WhatsApp-specific retry documentation. Maintain your own dead-letter and replay capability for already acknowledged events.
Secret rotation causes either outage or permissive verification. Test the exact overlap interval, new-key deployment, old-key revocation, and alerting. Record only secret version identifiers. Any unknown signature should fail closed.
Unknown schema field triggers a customer action. Treat unknown event types as data to store and inspect, not commands. Update the parser only after checking the current API reference and adding a fixture.
Operational metrics that answer useful questions
Avoid a single “webhook success rate” that hides the boundary between provider delivery and application processing. Track accepted requests, rejected signatures, validation failures, durable-write failures, acknowledgement latency, duplicate delivery count, queue age, worker retries, dead-letter volume, side-effect idempotency hits, replay count, and time from provider timestamp to final processing. Define each denominator. For instance, acknowledgement latency should be measured only for requests that reached the endpoint, while processing delay begins at the durable inbox receipt.
These metrics are operating signals, not claims about a benchmark. Choose alert thresholds from your service objective and observed baseline rather than copying a generic number. A sustained queue-age increase may be more actionable than a burst of duplicate deliveries; repeated duplicates with no extra side effects may be expected retry behavior, whereas a low duplicate count does not prove that events are not being lost. Pair count metrics with sampled, redacted traces that can connect an event through each stage.
Use a correlation ID generated by your service and preserve provider identifiers as separate fields. Avoid using a phone number or message body as a metric label; high-cardinality or sensitive labels can create both cost and privacy problems. A runbook should say who owns each alert, what evidence they inspect, and which replay command is safe. A replay should select stored inbox IDs, require a reason, record an operator, and preserve the original event; it should not reconstruct a webhook by hand from a screenshot.
Source control and evidence quality
The primary source for current product behavior is Meta's Developer Hub and linked API documentation. The documentation mirror is useful when direct access is rate-limited, but its publication/update date and relationship to the official source should remain visible. OWASP provides security recommendations, not a Meta contract. Stripe's idempotency guide provides an analogy, not a WhatsApp guarantee. The Stack Overflow discussion is a user report, not a statistically representative sample. CISA, OECD, and McKinsey links provide broader security or AI-operation context, not evidence for a specific header or retry interval.
When sources disagree, compare what each actually describes: product versus generic Graph API; delivery retry versus worker retry; webhook transport versus outbound API calls; current API version versus older SDK behavior. Do not average incompatible retry windows. If a source is stale, paywalled, mirrored, or inaccessible, record that limitation and avoid relying on an exact number that cannot be independently checked. A source close to a claim should make it easy for a maintainer to re-verify that claim after a version upgrade.
This research pass was conducted on 25 October 2026. Search angles included direct WhatsApp retry/authentication behavior, security and duplicate delivery controls, contrary reports about repeated old events, and practitioner questions. The main live Meta page returned HTTP 429; the mirror, OWASP page, official Developer Hub, Stripe documentation, and one Stack Overflow thread were read. A targeted video search did not surface a sufficiently relevant source to support a technical claim, so no video-derived assertion is included. This is an access and selection limitation, not proof that no useful video exists.
Decision table: what should the team do next?
| Current evidence | Decision | Do not do |
|---|---|---|
| Valid signature; inbox commit succeeded | Acknowledge, enqueue/dispatch, process asynchronously | Wait for CRM or AI response in the webhook route |
| Signature invalid or absent | Reject; emit redacted security event | Parse and act on the JSON anyway |
| Storage commit failed | Return documented retryable failure; alert | Return success because the payload looked valid |
| Same event key already stored | Acknowledge duplicate; suppress repeated side effect | Drop all later status events for that message |
| Older status arrives after newer state | Preserve event; apply deterministic projection rule | Replace current state using arrival order |
| Worker outcome is ambiguous | Check action ledger and reconcile before resend | Blindly replay an outbound action |
| Unknown field/event | Store or quarantine according to policy; inspect schema | Treat it as an instruction |
| Retry retention is unclear | Verify current WhatsApp-specific docs and test account | Import a generic Graph or third-party retry number |
The decision table is deliberately conservative. Its aim is to make unknowns visible and preserve evidence, not to guarantee zero failures. A system can still lose data through storage corruption, credential compromise, account configuration errors, or a downstream service with non-idempotent behavior. The acceptance test should expose those boundaries and assign an owner to each one.
Limits, contradictions, and conditions where this advice fails
The exact WhatsApp retry schedule and payload schema are version- and product-specific. The research could not directly fetch the current Meta documentation page because it returned 429; the mirrored content is secondary even if it reproduces official material. Re-check current docs before using the seven-day statement as a retention requirement. This article does not establish delivery ordering, a guaranteed event identifier for every event type, or a universal status transition model.
The sample harness uses an in-memory Map, so it cannot demonstrate durability or multi-process uniqueness. Its secret is a fixture string, not a production credential. Its event key is illustrative and must be adapted to the real schema. A passing unit test cannot replace a staging integration run, account subscription inspection, provider-side delivery logs, load testing, incident drills, or legal review of personal-data retention. OWASP guidance is general; a team still has to verify its provider contract and deployment architecture.
The recommendation may not fit a deliberately synchronous integration where the provider contract, latency budget, and business action all require an immediate response. Even then, keep the synchronous work bounded, make failures explicit, and test the ambiguous side-effect boundary. Some teams may use a managed webhook gateway that already persists deliveries; verify its delivery guarantee and export/replay path instead of duplicating its features. The goal is not to maximize components. It is to identify the single durable acknowledgement boundary and demonstrate that it behaves as intended.
Pre-production checklist
- [ ] Confirm the exact WhatsApp Cloud API version, subscribed fields, app, business account, and phone number ID.
- [ ] Verify the endpoint GET challenge separately from POST signature verification.
- [ ] Validate HMAC over the raw request bytes; reject tampering and missing signatures.
- [ ] Define and test the durable inbox or queue commit that must precede HTTP success.
- [ ] Exercise all twelve matrix cases in staging with synthetic, non-customer data.
- [ ] Deliver the same fixture twice and prove no second business side effect occurs.
- [ ] Deliver status events in reverse order and prove the projection does not regress.
- [ ] Inject storage outage and worker crashes at both sides of a side-effect boundary.
- [ ] Document retry ownership: provider retries before acknowledgement; application retries after durable acceptance.
- [ ] Confirm secret rotation, redacted logs, payload limits, access controls, and retention.
- [ ] Verify dead-letter inspection and audited replay without copying payloads into tickets.
- [ ] Save the run evidence packet, failed cases, mitigations, and owner sign-off.
Bottom line
Do not promote a WhatsApp webhook because the test endpoint returned 200. Promote it when a signed event is durably recorded before acknowledgement, a replay is harmless, out-of-order status delivery cannot silently regress state, storage failure remains retryable, and the worker can recover from ambiguous completion. The twelve-case matrix gives a team a concrete acceptance asset; the fixture tests raw-byte signing and duplicate behavior; the source notes show where product-specific claims still need verification.
The claim this article can support is modest but useful: an authenticated, durable inbox plus idempotent asynchronous processing is a testable design for handling retries and downstream outages. It cannot promise that Meta will deliver every event, that events arrive in order, or that an integration will never fail. Keep the provider contract current, keep your own recovery path, and make every acknowledgement decision auditable.
Top comments (0)