Skip to content
ReferenceIntegrations

Webhooks

Receive real-time updates from WhatsApp

DevelopersUpdated

There are two distinct webhook systems in Soosh:

  1. Outbound webhook subscriptions (/api/webhooks) — endpoints you own that Soosh 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 Soosh.

See the Webhooks feature guide for a walkthrough.

Register your own HTTPS endpoints to receive event notifications from Soosh. Soosh signs each delivery, retries failures, and enforces SSRF protection on the target URL.

GET/api/webhooks
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)
{
"status": "success",
"data": {
"webhooks": [
{
"id": "uuid",
"name": "Order events",
"url": "https://example.com/hooks/soosh",
"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.

POST/api/webhooks
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/soosh",
"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.

GET/api/webhooks/{id}
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/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.

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

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, Soosh 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: Soosh-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).

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

GET/api/webhook

Meta 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
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"
}
]
}
]
}
Status Description
sent Message sent to WhatsApp servers
delivered Message delivered to recipient
read Message read by recipient
failed Message 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:

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
{
"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

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