WEBHOOKS
HOW WEBHOOKS WORK
Every inbound WhatsApp event fires a POST request to the webhook_url you configure on each bridge. Your server receives the event in real time and responds with 200.
Set the webhook URL when provisioning a bridge:
curl -X POST https://wabridges.com/api/instances \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_ref":"user-123","webhook_url":"https://yourbackend.com/hook"}'
You can update the webhook URL at any time from the bridge detail page in your dashboard.
All events share a common shape: a JSON object with an event string field identifying the type. Switch on that field to route events to the right handler.
POST https://yourbackend.com/hook
Content-Type: application/json
{"event":"message", ...event-specific fields}
DELIVERY BEHAVIOR
Respond fast. Return 200 immediately and do any processing asynchronously. If your handler takes longer than 10 seconds the delivery is considered failed and the event is dropped.
Design your handler to be idempotent. While retries are not automatic, network failures at your end can cause duplicate deliveries in rare cases.
EVENT TYPES
message
Fields: event, message_id, contact_id, phone, chat_id, name, body, type, media_type, is_group, from_me, timestamp
{"event":"message","message_id":"ACE41E...","contact_id":"15550001234@s.whatsapp.net","phone":"15550001234","chat_id":"15550001234@s.whatsapp.net","name":"Alice","body":"Hello!","type":"text","media_type":"","is_group":false,"from_me":false,"timestamp":1777180605}
connected
Fields: event, phone
{"event":"connected","phone":"15550001234"}
disconnected
Fields: event
{"event":"disconnected"}
typing
Fields: event, contact_id, chat_id, state (composing|paused), mode (text|voice)
{"event":"typing","contact_id":"15550001234@s.whatsapp.net","chat_id":"15550001234@s.whatsapp.net","state":"composing","mode":"text"}
poll_vote
Fields: event, contact_id, phone, chat_id, poll_id, message_id, name, from_me, timestamp, selected_options
{"event":"poll_vote","contact_id":"15550001234@s.whatsapp.net","phone":"15550001234","chat_id":"15550001234@s.whatsapp.net","poll_id":"ACPOLL0001","message_id":"ACVOTE0001","name":"Alice","from_me":false,"timestamp":1777330500,"selected_options":["Red"]}
event_response
Fields: event, contact_id, phone, chat_id, event_id, message_id, name, from_me, timestamp, response (going|not_going|maybe), extra_guest_count
{"event":"event_response","contact_id":"15550001234@s.whatsapp.net","phone":"15550001234","chat_id":"15550009999@g.us","event_id":"ACEVENT0001","message_id":"ACEVRESP0001","name":"Alice","from_me":false,"timestamp":1777330700,"response":"going","extra_guest_count":0}
call_incoming
Fields: event, call_id, contact_id, platform, timestamp
{"event":"call_incoming","call_id":"ABCDEF123456","contact_id":"15550001234@s.whatsapp.net","platform":"android","timestamp":1777180605}
call_terminated
Fields: event, call_id, contact_id, reason (timeout|hangup|decline|busy), timestamp
{"event":"call_terminated","call_id":"ABCDEF123456","contact_id":"15550001234@s.whatsapp.net","reason":"hangup","timestamp":1777180720}
offline_sync_preview
Fields: event, total, messages, notifications, receipts, app_data_changes
{"event":"offline_sync_preview","total":142,"messages":120,"notifications":8,"receipts":14,"app_data_changes":0}
offline_sync_completed
Fields: event, count
{"event":"offline_sync_completed","count":142}
profile_picture_updated
Fields: event, remove (bool), picture_id?, timestamp
{"event":"profile_picture_updated","remove":false,"picture_id":"12345678901","timestamp":1777180605}
HANDLING WEBHOOKS
The pattern is the same in every language: parse the body, switch on event, return 200 before doing any heavy work.
Node.js (Express)
const express = require('express')
const app = express()
app.use(express.json())
app.post('/hook', (req, res) => {
res.sendStatus(200) // respond immediately
const { event, ...data } = req.body
switch (event) {
case 'message':
if (!data.from_me) {
console.log(`${data.name}: ${data.body}`)
// reply, store, trigger workflow...
}
break
case 'connected':
console.log(`bridge connected, phone=${data.phone}`)
break
case 'disconnected':
console.warn('bridge disconnected')
break
case 'typing':
// data.state = 'composing' | 'paused'
break
}
})
app.listen(3000)
Python (Flask)
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
def process_event(payload):
event = payload.get('event')
if event == 'message' and not payload.get('from_me'):
print(f"{payload['name']}: {payload['body']}")
# reply, store, trigger workflow...
elif event == 'connected':
print(f"bridge connected, phone={payload['phone']}")
elif event == 'disconnected':
print('bridge disconnected')
@app.route('/hook', methods=['POST'])
def webhook():
payload = request.get_json()
# process async so we return 200 immediately
threading.Thread(target=process_event, args=(payload,)).start()
return '', 200
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
$payload = json_decode(file_get_contents('php://input'), true);
http_response_code(200); // respond immediately
$event = $payload['event'] ?? '';
switch ($event) {
case 'message':
if (empty($payload['from_me'])) {
error_log("{$payload['name']}: {$payload['body']}");
// reply, store, trigger workflow...
}
break;
case 'connected':
error_log("bridge connected, phone={$payload['phone']}");
break;
case 'disconnected':
error_log('bridge disconnected');
break;
}