Skip to content

Webhooks

There are two distinct webhook systems in Whatomate:

  1. 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.
  2. 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.

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.

Terminal window
GET /api/webhooks
ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerPage size, 1–100. Default 50.
searchstringFilter by name or URL (case-insensitive)
{
"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.

Terminal window
POST /api/webhooks
FieldTypeRequiredDescription
namestringYesHuman-readable label
urlstringYesTarget URL (http or https; must not resolve to an internal/private address)
eventsstring[]YesAt least one event type (see below)
headersobjectNoCustom headers sent with every delivery
secretstringNoHMAC signing secret. If omitted, one is auto-generated
is_activebooleanNoWhether 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"
}

Returns the created webhook in the same shape as a List item.

Terminal window
GET /api/webhooks/{id}
Terminal window
PUT /api/webhooks/{id}

Accepts the same body as Create. Sending an empty secret leaves the existing secret unchanged; is_active is always applied.

Terminal window
DELETE /api/webhooks/{id}
{
"status": "success",
"data": { "message": "Webhook deleted successfully" }
}

Send a synchronous test event to the webhook's URL to verify connectivity and signing.

Terminal window
POST /api/webhooks/{id}/test

The 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" }
}
EventDescription
message.incomingA new message is received from a contact
message.sentAn agent sends a message
message.outgoingA message is sent to a contact (includes echoes)
contact.createdA new contact is created
transfer.createdA transfer to a human agent is requested
transfer.assignedA transfer is assigned to an agent
transfer.resumedThe chatbot is resumed (transfer closed)

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).

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.

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.

Webhook URLs are validated on save and again at delivery time:

  • The scheme must be http or https.
  • Hostnames like localhost, *.local, and *.internal are rejected.
  • IP literals and DNS results that resolve to loopback, private, link-local, or unspecified ranges are refused (guards against DNS-rebinding).

Whatomate exposes webhook endpoints that you configure in your Meta App settings:

Terminal window
GET /api/webhook

Meta sends a verification request when setting up webhooks:

ParameterDescription
hub.modeAlways "subscribe"
hub.verify_tokenYour configured verify token
hub.challengeChallenge string to return
Terminal window
POST /api/webhook

All WhatsApp events are sent to this endpoint.

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"
}
]
}
]
}

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"
}
]
}
]
}
StatusDescription
sentMessage sent to WhatsApp servers
deliveredMessage delivered to recipient
readMessage read by recipient
failedMessage failed to deliver

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.

Every frame is { "type": ..., "payload": ... }. Types use snake_case:

GroupTypes
Sessionauth, set_contact, ping, pong
Messagingnew_message, status_update, reaction_update, contact_update
Notesconversation_note_created, conversation_note_updated, conversation_note_deleted
Agent transfersagent_transfer, agent_transfer_assign, agent_transfer_resume, transfer_escalation, transfer_escalated, transfer_expired
Campaignscampaign_stats_update
Permissionspermissions_updated
Callscall_incoming, call_answered, call_ended, call_hold, call_resumed, call_permission_update
Call transferscall_transfer_waiting, call_transfer_connected, call_transfer_completed, call_transfer_abandoned, call_transfer_no_answer, call_transfer_reassigned
Outgoing callsoutgoing_call_initiated, outgoing_call_ringing, outgoing_call_answered, outgoing_call_rejected, outgoing_call_ended
{
"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"
}
}

Always verify webhook requests are from Meta:

  1. Check the X-Hub-Signature-256 header
  2. Compute HMAC-SHA256 of the request body using your app secret
  3. 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.

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.