How Switchbord receives webhooks
Meta delivers webhook events to two endpoints on your Switchbord deployment:POST /webhooks/whatsapp— the primary WhatsApp webhook receiverPOST /webhooks/meta— an alternate Meta webhook receiver
Webhook challenge verification
Before Meta starts sending events to a new webhook URL, it performs a challenge verification. Meta sends aGET request to your webhook URL with a hub.challenge parameter and expects you to echo it back.
Switchbord handles this automatically at GET /webhooks/whatsapp. When you configure your META_WEBHOOK_VERIFY_TOKEN, Switchbord validates Meta’s token and responds with the challenge. You do not need to write any custom verification logic.
The challenge handshake only happens once when you register or update your webhook URL in Meta’s Business Manager. After that, all subsequent requests are
POST deliveries.Signature verification
EveryPOST webhook delivery from Meta includes an X-Hub-Signature-256 header. This header contains an HMAC-SHA256 signature computed over the raw request body using your Meta app’s signing secret.
Switchbord verifies this signature on every delivery:
Read the raw body
Switchbord reads the raw request body before any parsing. The signature must be verified against the original bytes, not a re-serialized version.
Compute the expected signature
Switchbord computes
HMAC-SHA256(raw_body, WEBHOOK_SIGNATURE_SECRET) and formats it as sha256=<hex>.Compare signatures
Switchbord compares the computed signature against the value in
X-Hub-Signature-256 using a constant-time comparison to prevent timing attacks.Envelope storage
After verification, Switchbord stores the complete envelope before doing anything else. The stored record includes:- The event source and topic (Meta, Emarsys, etc.)
- The WABA ID and phone number scope
- The verification result
- Request headers and query parameters
- A hash of the raw body (for integrity checks)
- Response status and timing metadata
Event normalization
After storing the raw envelope, Switchbord normalizes the delivery into one of its known event families:| Event family | What it represents |
|---|---|
message.inbound | A new message received from a contact |
message.status | A delivery status update (sent, delivered, read, failed) |
template.updated | A template approval status change from Meta |
conversation.updated | A conversation-level change such as a window opening or closing |
message.inbound event creates or updates a conversation; a message.status event appends to the message’s delivery history.
Replay
If a webhook fails processing — for example, because a downstream dependency was temporarily unavailable — you can replay it from the operator dashboard.Find the failed event
Navigate to the webhook events view and filter by status. Failed events show the error that caused the failure.
Inspect the envelope
Review the stored headers and body hash to confirm this is the event you want to reprocess.
Trigger the replay
Click Replay to queue the event for reprocessing. Switchbord creates a new attempt record against the original envelope — the original record is not mutated.
Inspecting webhook events
The operator dashboard and internal API give you full visibility into webhook deliveries:| Endpoint | What it returns |
|---|---|
GET /internal/webhooks | Paginated list of webhook deliveries, filterable by type and status |
GET /internal/webhooks/:id | Full envelope details including headers, body hash, and normalization status |
GET /internal/webhook-rejections | Deliveries that failed signature verification |
GET /internal/webhook-rejections/:id | Full details for a specific rejected delivery |
Performance expectations
Switchbord is designed to handle webhook ingestion with minimal latency. The target is a median response time under 250 milliseconds for the ingestion path, with fewer than 1% of requests taking longer than one second. The ingestion path does the minimum necessary work synchronously — verify, store, enqueue — and defers all processing to the background worker. This keeps the endpoint fast even under high volume.Switchbord tracks rejection counts, replay counts, queue depth, and worker heartbeat. If ingestion latency or rejection rates spike, check the runtime diagnostics at
GET /internal/runtime.