Point a bridge at your URL and every inbound WhatsApp message arrives as JSON - the payload, the rules your endpoint must follow, and how to prove a delivery is really ours.
Inbound messages are pushed to you, not polled. Set webhook_url when you create the bridge, or change it later - the bridge keeps running and the next event goes to the new URL.
Until you set one, events land in the hosted inbox on the dashboard. That is the fastest way to see the shape of real traffic before you have written a handler.
curl -X PATCH https://wabridges.com/api/instances/user-123 \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://yourbackend.com/hook"}'
A POST of JSON, one event per request. Every inbound message the bridge sees fires this, including messages sent from the linked phone itself - those carry from_me: true, which is the flag that stops a bot answering itself.
chat_id is where to reply. contact_id is who wrote it. In a one-to-one chat they are the same; in a group they are not, and confusing them is the most common way a first integration replies to the wrong place.
{
"event": "message",
"type": "text",
"body": "hello",
"chat_id": "15550001234@s.whatsapp.net",
"contact_id": "15550001234@s.whatsapp.net",
"from_me": false,
"is_group": false,
"media_type": "",
"message_id": "ACE41E988162AB19A4A7BB7A9E663693",
"name": "John Doe",
"phone": "15550001234",
"timestamp": 1777180605
}
message is one of several events. connected and disconnected tell you the phone link came up or went down - see stay connected for which drops heal themselves and which need a person.
Return 2xx immediately and do the real work asynchronously. If your handler calls a model or a slow API before responding, deliveries queue behind it and WhatsApp senders see nothing happen.
Delivery is at-least-once and in order. A failed delivery - network error, 429, or any 5xx - is retried up to five times with backoff at 10s, 30s, 1m, 3m. Any other 4xx is never retried, so a stray 404 or 403 from your side drops that event for good.
Because retries exist, the same event can arrive twice. Deduplicate on X-Webhook-Event-Id, which stays the same across every retry of one event; X-Webhook-Attempt tells you which try you are looking at, 1-based.
The delivery log on the bridge page records every attempt with its status code, latency and error - the first place to look when an event did not arrive.
Your webhook URL is a public endpoint, so authenticate every delivery before trusting it. Each one is signed two ways with the bridge's webhook_secret.
An Authorization: Bearer header carries the secret directly, which is what no-code tools like n8n's Webhook node verify natively. Alongside it, X-Webhook-Signature: t=<unix>,v1=<hex> is an HMAC-SHA256 over <t>.<raw body> keyed with the secret. Prefer the signature, compare it in constant time, and reject anything whose t is more than about five minutes old so a captured delivery cannot be replayed at you later.
Sign against the raw request body, before any JSON parsing or re-serialisation. Re-encoding changes the bytes and the signature will never match.
Reply by posting to the same bridge with the event's chat_id as chat - see send a message. The two calls together are the entire loop; everything else in the API is detail on top of it.
Something here not matching what you see? Write to us - a person answers.