Receiving WhatsApp messages at a webhook: payloads, signatures, retries and the 24-hour rule
Everything a WhatsApp Business Platform number receives arrives through a webhook: inbound messages, delivery and read statuses, template status changes, account alerts. Getting the endpoint right is most of the integration.
What Meta sends
Meta posts JSON to one URL per app. Each post has an entry list; each entry has changes; each change has a field (usually messages) and a value holding the phone number id the event belongs to, optional contacts, and either messages (inbound) or statuses (sent, delivered, read, failed). One post can carry several messages, and the same message can be posted more than once.
Verifying it is really Meta
Every post carries X-Hub-Signature-256: sha256= followed by an HMAC-SHA256 of the raw body using the app secret. Compute it over the raw bytes (not the re-serialised JSON) and compare with a constant-time function. Reject anything that fails. On first setup Meta also sends a GET with hub.verify_token and hub.challenge; echo the challenge only if the token matches.
Idempotency
Store the message id (wamid…) with a unique constraint and ignore repeats. Statuses can arrive out of order (a read before a delivered), so apply them as a maximum, not a sequence.
Respond fast, work later
Meta expects a 200 within a few seconds. Write to a queue or a database and return; do the CRM lookup, the AI call, the Tally update afterwards. Slow endpoints get retried, and persistently slow ones get the app's webhook disabled.
What a connector adds
With a multi-tenant connector like NumberBridge there is one Meta webhook for all customers. The connector verifies Meta's signature, finds the tenant by phone number id, stores the message, and forwards a cleaner event to the tenant's own URL:
POST https://yourapp.com/hooks/whatsapp
X-NumberBridge-Signature: <hex HMAC-SHA256 of the body with your secret>
X-NumberBridge-Event: message
{"event":"message",
"connection":{"phone_number_id":"1493…","display_phone":"+91 95917 24716"},
"contact":{"wa_id":"919716019716","profile":{"name":"Mahavir Traders"}},
"message":{"id":"wamid.HBg…","from":"919716019716","timestamp":1760000000,"type":"text","text":{"body":"ledger bhej do"}}}
The forward is retried three times with backoff and every attempt is written to a delivery log you can read, so a bad deploy on your side does not lose a day of messages.
The 24-hour rule, in webhook terms
Each inbound message opens or extends a 24-hour service window for that customer. Inside it you may send free-form text, media and documents. Outside it only an approved template is accepted; the API returns an error otherwise. Store the timestamp of the last inbound message per customer and your send code can choose the right path without guessing.
Minimal endpoint
from fastapi import FastAPI, Request, Header
import hmac, hashlib
app = FastAPI()
SECRET = "your webhook secret"
@app.post("/hooks/whatsapp")
async def hook(request: Request, x_numberbridge_signature: str = Header("")):
raw = await request.body()
good = hmac.compare_digest(hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest(), x_numberbridge_signature)
if not good:
return {"ok": False}
event = await request.json()
queue.put(event) # handle later
return {"ok": True}
Put software on your business number without losing the phone
NumberBridge connects an existing WhatsApp Business app number to the Cloud API in two minutes. Flat monthly fee, no per-message markup, 14-day trial.
Related guides
- What is WhatsApp coexistence? Keep the number on the phone and use the API
- WhatsApp Business app vs WhatsApp Business API vs coexistence: which one you need
- How to connect an existing WhatsApp Business app number to the Cloud API without losing the phone
- What the WhatsApp Business Platform really costs in India, and what a connector should not charge
- Build a WhatsApp AI agent for a small business: the architecture that works
- Tally on WhatsApp: send ledgers, invoices and payment reminders from the business number automatically