Description
What is webhook acknowledgement?
Webhook acknowledgement is your side of the delivery contract with Meta. For every POST to your webhook endpoint, Meta expects an HTTP 200 status. Anything else — a 4xx, a 5xx, a redirect, a dropped connection or a request that simply takes too long — counts as a failed delivery, and Meta retries it with decreasing frequency for up to seven days. The acknowledgement means "I have safely received this", not "I have finished handling it".
That leads to the acknowledge-then-process pattern. The request handler does only three cheap things: verify the X-Hub-Signature-256 signature over the raw body, write the payload to something durable (a queue, a log or a table), and return 200. Everything slow — CRM updates, chatbot or LLM calls, media downloads, database joins — happens afterwards in background workers. The handler's response time then stays flat however heavy the downstream work becomes.
Storing before answering matters because Meta offers no API to fetch historical webhook data. Once you have returned 200, Meta considers the event delivered and will not send it again, so an event acknowledged but not yet persisted is lost if the process crashes. Acknowledgement is also different from the webhook retry policy: the retry policy is what Meta does when your acknowledgement does not arrive, while acknowledgement is how you make retries rare in the first place.
Key takeaways
- Webhook acknowledgement means answering every WhatsApp webhook POST with HTTP 200 once it is verified and durably stored.
- Any other outcome — an error status, a redirect, a timeout or a dropped connection — makes Meta retry for up to seven days.
- Verify the signature, persist the raw payload, return 200, and leave every slow task to background workers.
- Meta has no API to fetch past webhooks, so an event acknowledged before it is stored is lost for good.
- Return 503 only when you could not store the event; answer 200 for fields you ignore and events you cannot parse.
Mechanics
How does webhook acknowledgement work?
step 1 — Read the raw body
Capture the request bytes exactly as received. The signature is computed over them, and you will store them unchanged.
step 2 — Verify the signature
Recompute the HMAC-SHA256 of the raw body with your app secret and compare it with
X-Hub-Signature-256in constant time. Reject mismatches with401or403.step 3 — Persist durably
Write the payload to a durable message queue or an append-only table before answering. This is the only copy you will ever have.
step 4 — Return 200 immediately
Send
200with an empty or minimal body. Do not wait for processing, and do not vary the status by event type — answer200for fields you ignore too.step 5 — Process asynchronously
Workers read from the queue, apply webhook deduplication, and run side effects. Failures are retried by your own queue and parked in a dead-letter queue — never pushed back onto Meta.
step 6 — Watch the acknowledgement path
Alert on p99 response time and on the share of non-200 responses. Either creeping up is the early warning of a retry storm.
Usage
Webhook acknowledgement in code
import crypto from "node:crypto";import express from "express";const app = express();// Keep the raw bytes and allow Meta's 3 MB maximum payload.app.use(express.raw({ type: "application/json", limit: "4mb" }));app.post("/webhooks/whatsapp", async (req, res) => {const expected = "sha256=" + crypto.createHmac("sha256", process.env.APP_SECRET).update(req.body).digest("hex");const given = req.get("X-Hub-Signature-256") ?? "";if (given.length !== expected.length ||!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {return res.sendStatus(401);}try {await inbox.insert({ raw: req.body.toString("utf8"), receivedAt: new Date() });} catch (err) {return res.sendStatus(503); // not stored, so let Meta retry}return res.sendStatus(200); // stored: acknowledge, process later});
Verify, persist, acknowledge. A failed write returns 503 on purpose so Meta's retry keeps the event alive.
Properties
Webhook acknowledgement: key facts and limits
- Success response
- HTTP 200
- Anything else
- Treated as failure and retried
- Retry window
- Decreasing frequency, up to 7 days
- Maximum payload
- 3 MB
- Updates per request
- Up to 1,000 (batching not guaranteed)
- Published timeout
- None — answer in milliseconds, not seconds
- History API
- None: store events before acknowledging
Caveats
Common mistakes with webhook acknowledgement
Doing the work before answering
Database joins, CRM calls and LLM replies inside the request make response time depend on other systems. Move them to workers and keep the handler to verify, store, answer.
Acknowledging before storing
Returning 200 and then enqueuing in memory loses events on every crash or deploy, and Meta will not resend them. Persist first, then answer.
Answering 202 or 204
Meta's documentation asks for 200 specifically; no other value among the HTTP status codes is documented as success, and some frameworks default to 204 for empty responses. Set the status explicitly rather than relying on another 2xx being accepted.
Turning business errors into HTTP errors
Returning 404 for an unknown customer or 422 for an event you cannot parse makes Meta retry it for days. Store it, return 200, and handle the problem in your own dead-letter queue.
Why it matters
Why does webhook acknowledgement matter?
The acknowledgement decides whether the inbound pipeline flows or snowballs. A handler that calls a CRM before answering is fast on a quiet afternoon and fails during a broadcast's status wave: responses slow down, Meta treats them as failures, retries pile on top of new traffic and duplicates multiply. Acknowledging in milliseconds keeps Meta's view of your endpoint healthy, isolates customer-facing replies from slow internal systems and turns every downstream outage into a backlog you drain, rather than a retry storm you fight.
Example
Example: A results-morning status wave in Hyderabad
A Hyderabad diagnostics chain sends 60,000 "report ready" utility templates at 7 am. Each message can produce sent, delivered and read statuses, so status webhooks peak for the next hour. Its old handler updated the patient record and called the CRM before answering, often taking over two seconds; deliveries timed out, Meta retried, and the same statuses were processed three or four times. After moving to acknowledge-then-process — verify, insert into a queue table, return 200 — the handler answers in about 15 ms, the queue absorbs the peak, and workers catch up within minutes (figures illustrative).
Questions
Webhook Acknowledgement FAQs
- What should my webhook return when the signature check fails?
- Return 401 or 403. A genuine Meta request only fails the check if your app secret is wrong, so alert on a sudden rise — otherwise you will reject real events for as long as Meta keeps retrying them.
- What happens if processing fails after I returned 200?
- Meta will not resend it; to Meta the event was delivered. Your own queue must retry the work and park repeated failures in a dead-letter queue for inspection.
- How fast does a WhatsApp webhook acknowledgement need to be?
- Meta does not publish a timeout, so answer in milliseconds. Verify-store-answer needs no network calls beyond one durable write; treat anything near a second as a warning sign that slow work has crept into the request.
- Should I return 202 Accepted because I process the event later?
- No, return 200. Meta's documentation asks for a 200 OK on every notification, and the status only means you have safely received the event — not that you have finished processing it.
Notes
Best practices for webhook acknowledgement
- Give the acknowledgement path its own process or pool, so a busy worker fleet can never slow it down.
- Set the body-size limit above 3 MB on every proxy and framework in front of the handler.
- Return 503 only when you genuinely could not store the event — that is the one case where a retry helps.
- Load-test with a realistic status wave, not a trickle of single messages.
- If several systems need each event, use webhook fan-out from the queue after the 200 — never inside the request.