Webhooks
Overview
Section titled “Overview”There are two distinct webhook systems in Whatomate:
- Outbound webhook subscriptions (
/api/webhooks) — endpoints you own that Whatomate calls when events happen (new message, contact created, transfer requested, etc.). Managed via the REST API below. - Inbound Meta webhook (
/api/webhook, singular) — the endpoint you configure in your Meta App so WhatsApp delivers incoming messages and status updates to Whatomate.
See the Webhooks feature guide for a walkthrough.
Outbound Webhook Subscriptions
Section titled “Outbound Webhook Subscriptions”Register your own HTTPS endpoints to receive event notifications from Whatomate. Whatomate signs each delivery, retries failures, and enforces SSRF protection on the target URL.
List Webhooks
Section titled “List Webhooks”GET /api/webhooksQuery Parameters
Section titled “Query Parameters”| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
limit | integer | Page size, 1–100. Default 50. |
search | string | Filter by name or URL (case-insensitive) |
Response
Section titled “Response”{ "status": "success", "data": { "webhooks": [ { "id": "uuid", "name": "Order events", "url": "https://example.com/hooks/whatomate", "events": ["message.incoming", "transfer.created"], "headers": { "X-Team": "support" }, "is_active": true, "has_secret": true, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ], "available_events": [ { "value": "message.incoming", "label": "Message Incoming", "description": "When a new message is received from a contact" } ], "total": 1, "page": 1, "limit": 50 }}The has_secret flag indicates whether a signing secret is configured; the
secret itself is never returned.
Create Webhook
Section titled “Create Webhook”POST /api/webhooksRequest Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable label |
url | string | Yes | Target URL (http or https; must not resolve to an internal/private address) |
events | string[] | Yes | At least one event type (see below) |
headers | object | No | Custom headers sent with every delivery |
secret | string | No | HMAC signing secret. If omitted, one is auto-generated |
is_active | boolean | No | Whether the webhook is enabled |
{ "name": "Order events", "url": "https://example.com/hooks/whatomate", "events": ["message.incoming", "transfer.created"], "headers": { "X-Team": "support" }, "secret": "my-signing-secret"}Response
Section titled “Response”Returns the created webhook in the same shape as a List item.
Get Webhook
Section titled “Get Webhook”GET /api/webhooks/{id}Update Webhook
Section titled “Update Webhook”PUT /api/webhooks/{id}Accepts the same body as Create. Sending an empty secret leaves the existing
secret unchanged; is_active is always applied.
Delete Webhook
Section titled “Delete Webhook”DELETE /api/webhooks/{id}{ "status": "success", "data": { "message": "Webhook deleted successfully" }}Test Webhook
Section titled “Test Webhook”Send a synchronous test event to the webhook's URL to verify connectivity and signing.
POST /api/webhooks/{id}/testThe endpoint receives a payload with "event": "test". On a non-2xx response the
API returns 502 Bad Gateway.
{ "status": "success", "data": { "message": "Test webhook sent successfully" }}Event Types
Section titled “Event Types”| Event | Description |
|---|---|
message.incoming | A new message is received from a contact |
message.sent | An agent sends a message |
message.outgoing | A message is sent to a contact (includes echoes) |
contact.created | A new contact is created |
transfer.created | A transfer to a human agent is requested |
transfer.assigned | A transfer is assigned to an agent |
transfer.resumed | The chatbot is resumed (transfer closed) |
Delivery Payload
Section titled “Delivery Payload”Every delivery is a POST with this envelope:
{ "event": "message.incoming", "timestamp": "2024-01-01T12:00:00Z", "data": { }}The data object varies by event (message, contact, or transfer details).
Signing
Section titled “Signing”When a secret is configured, Whatomate signs the raw request body and sends the
signature in the X-Webhook-Signature header:
X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the body>Verify it by computing HMAC-SHA256(body, secret) and comparing (constant-time)
against the value after the sha256= prefix.
Retries
Section titled “Retries”A delivery is attempted at most 3 times total (the initial attempt plus 2 retries), with a 2s and then 4s pause before each retry. A network error or any non-2xx response counts as a failure; after the third failure the delivery is dropped and logged.
Each dispatch sends to at most 10 webhooks concurrently. Retries mean your endpoint can receive the same event more than once — make it idempotent.
Every request carries Content-Type: application/json and User-Agent: Whatomate-Webhook/1.0,
plus any custom headers you configured.
SSRF Protection
Section titled “SSRF Protection”Webhook URLs are validated on save and again at delivery time:
- The scheme must be
httporhttps. - Hostnames like
localhost,*.local, and*.internalare rejected. - IP literals and DNS results that resolve to loopback, private, link-local, or unspecified ranges are refused (guards against DNS-rebinding).
Meta Webhook Configuration
Section titled “Meta Webhook Configuration”Whatomate exposes webhook endpoints that you configure in your Meta App settings:
Verification Endpoint
Section titled “Verification Endpoint”GET /api/webhookMeta sends a verification request when setting up webhooks:
| Parameter | Description |
|---|---|
hub.mode | Always "subscribe" |
hub.verify_token | Your configured verify token |
hub.challenge | Challenge string to return |
Event Endpoint
Section titled “Event Endpoint”POST /api/webhookAll WhatsApp events are sent to this endpoint.
Webhook Events
Section titled “Webhook Events”Incoming Message
Section titled “Incoming Message”Triggered when a new message is received.
{ "object": "whatsapp_business_account", "entry": [ { "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", "changes": [ { "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "1234567890", "phone_number_id": "PHONE_NUMBER_ID" }, "contacts": [ { "profile": { "name": "John Doe" }, "wa_id": "1234567890" } ], "messages": [ { "from": "1234567890", "id": "wamid.xxx", "timestamp": "1234567890", "type": "text", "text": { "body": "Hello!" } } ] }, "field": "messages" } ] } ]}Message Status Update
Section titled “Message Status Update”Triggered when a message status changes.
{ "object": "whatsapp_business_account", "entry": [ { "changes": [ { "value": { "statuses": [ { "id": "wamid.xxx", "status": "delivered", "timestamp": "1234567890", "recipient_id": "1234567890" } ] }, "field": "messages" } ] } ]}Status Values
Section titled “Status Values”| Status | Description |
|---|---|
sent | Message sent to WhatsApp servers |
delivered | Message delivered to recipient |
read | Message read by recipient |
failed | Message failed to deliver |
WebSocket Events
Section titled “WebSocket Events”For real-time updates in your frontend, connect to /ws. The socket upgrades unauthenticated —
the token is never passed in the query string. Fetch a short-lived token from
GET /api/auth/ws-token and send it as the first message:
const { data } = await (await fetch('/api/auth/ws-token')).json();
const ws = new WebSocket('ws://your-server:8080/ws');ws.onopen = () => ws.send(JSON.stringify({ type: 'auth', payload: { token: data.token } }));ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log('Event:', msg.type, msg.payload);};The token expires after 30 seconds, so fetch a fresh one on every (re)connect.
Event Types
Section titled “Event Types”Every frame is { "type": ..., "payload": ... }. Types use snake_case:
| Group | Types |
|---|---|
| Session | auth, set_contact, ping, pong |
| Messaging | new_message, status_update, reaction_update, contact_update |
| Notes | conversation_note_created, conversation_note_updated, conversation_note_deleted |
| Agent transfers | agent_transfer, agent_transfer_assign, agent_transfer_resume, transfer_escalation, transfer_escalated, transfer_expired |
| Campaigns | campaign_stats_update |
| Permissions | permissions_updated |
| Calls | call_incoming, call_answered, call_ended, call_hold, call_resumed, call_permission_update |
| Call transfers | call_transfer_waiting, call_transfer_connected, call_transfer_completed, call_transfer_abandoned, call_transfer_no_answer, call_transfer_reassigned |
| Outgoing calls | outgoing_call_initiated, outgoing_call_ringing, outgoing_call_answered, outgoing_call_rejected, outgoing_call_ended |
Message Event Payload
Section titled “Message Event Payload”{ "type": "new_message", "payload": { "id": "uuid", "contact_id": "uuid", "direction": "incoming", "message_type": "text", "content": { "body": "Hello!" }, "status": "received", "created_at": "2024-01-01T12:00:00Z" }}Security
Section titled “Security”Webhook Verification
Section titled “Webhook Verification”Always verify webhook requests are from Meta:
- Check the
X-Hub-Signature-256header - Compute HMAC-SHA256 of the request body using your app secret
- Compare with the signature in the header
Whatomate does this automatically — but only when both the X-Hub-Signature-256 header is
present and the WhatsApp account matching the payload's phone_number_id has an App Secret
stored (Settings → Accounts). If either is missing, the payload is accepted without verification.
Set the App Secret on every account you expose to Meta.
Rate Limiting
Section titled “Rate Limiting”Inbound Meta webhooks are processed synchronously in the request handler — there is no Redis
queue in front of /api/webhook. The Redis job queue is used for outbound campaign sends, not for
inbound webhook intake. Chatbot settings, flows, keyword rules and account lookups are Redis-cached
to keep per-webhook work small.
/api/webhook is exempt from authentication and from the global API rate limit.