Webhooks

Receive real-time WhatsApp events — inbound messages, receipts, connection state, calls, groups and more — pushed to your server.

Webhooks let your application receive WhatsApp events the moment they happen, instead of polling the API. When a configured event occurs on a device, WUTS sends an HTTP POST to the URL you registered, carrying a JSON payload that describes exactly what happened. Common uses:

  • Inbound messages — react to texts, media, locations, buttons and list replies.
  • Delivery receipts — track when outbound messages are delivered, read or played.
  • Connection / session state — know when a device connects, disconnects, logs out or is paired.
  • Calls — be notified of incoming call offers, accepts, rejections and terminations.
  • Groups & contacts — group info changes, joins, presence and profile updates.

All webhook endpoints require the standard bearer token — see Authentication. Configuration is scoped to the device that owns the token.

Configure a webhook

Send a POST /webhook with the target URL and the events you want delivered. Calling it again updates the existing configuration for the device.

curl -X POST https://api.wuts.com.br/webhook \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-app.com/wuts/webhook",
    "enabled_events": [
      "message.received",
      "message.delivered",
      "message.read",
      "device.connected",
      "device.disconnected"
    ],
    "enabled": true,
    "retry_attempts": 3,
    "timeout": 10,
    "secret": "a-long-random-string"
  }'
FieldTypeRequiredDescription
webhook_urlstringyesHTTPS URL that will receive the POST callbacks.
enabled_eventsstring[]yesOne or more event names (see the catalog). At least one is required.
enabledbooleannoWhether delivery is active. Defaults to true.
retry_attemptsintegernoRetries on failure. Defaults to 3.
timeoutintegernoPer-request timeout in seconds. Defaults to 10.
secretstringnoKey used to sign each payload with HMAC-SHA256.
headersobjectnoCustom headers added to every callback request.

A successful response returns the stored configuration, where webhook echoes back id, webhook_url, enabled_events, enabled, retry_attempts, timeout, headers, created_at, and updated_at:

{ "success": true, "message": "Webhook configured successfully", "webhook": { "...": "..." } }

Passing an event name that is not in the catalog returns 400 with an available_events array listing every valid name. Validate against GET /webhook/events before configuring.

Read, list and remove

# Current configuration for the device (404 if none is set)
curl https://api.wuts.com.br/webhook \
  -H "Authorization: Bearer <token>"

# Every event name you can subscribe to
curl https://api.wuts.com.br/webhook/events \
  -H "Authorization: Bearer <token>"

# Stop and delete the configuration
curl -X DELETE https://api.wuts.com.br/webhook \
  -H "Authorization: Bearer <token>"

GET /webhook returns the stored configuration (or 404 with "error": "Webhook not configured" when none is set), and GET /webhook/events returns the full list of subscribable event names:

{ "success": true, "webhook": { "...": "..." } }
{ "success": true, "events": ["message", "message.ack", "..."] }

Delivery model

Callbacks are pushed in real time as a POST with a JSON body. WUTS treats any 2xx status as success; any other status, a connection error, or exceeding the configured timeout triggers a retry.

  • Retries follow retry_attempts with an incremental backoff (e.g. 1s, 2s, 3s).
  • Custom headers (if configured) are sent on every attempt.
  • Only events listed in enabled_events are delivered; everything else is dropped.

Respond quickly with 200 and process asynchronously. Slow handlers count against timeout and cause unnecessary retries — and your endpoint should be idempotent, since a retried delivery may arrive more than once.

Payload envelope

Every callback shares the same outer shape. The data object varies by event.

{
  "id": "evt-3f2b9c1a",
  "event": "message.received",
  "timestamp": 1781827200,
  "device_id": "device-uuid",
  "data": { },
  "signature": "sha256=..."
}
FieldTypeDescription
idstringUnique id of this event delivery.
eventstringEvent name from the catalog.
timestampintegerUnix epoch seconds when the event was emitted.
device_idstringDevice that produced the event.
dataobjectEvent-specific payload (examples below).
signaturestringPresent only when a secret is configured (see Verifying).

Example: inbound message

{
  "id": "evt-3f2b9c1a",
  "event": "message.received",
  "timestamp": 1781827200,
  "device_id": "device-uuid",
  "data": {
    "message_id": "3EB0F8A1B2C3D4E5F6",
    "from": "5511999999999@s.whatsapp.net",
    "to": "5511888888888@s.whatsapp.net",
    "content": "Hi there!",
    "message_type": "text",
    "is_from_me": false,
    "push_name": "Maria",
    "timestamp": "2026-06-15T12:00:00Z"
  }
}

Media messages include a media object (with mime_type, caption and a stable media_path you can fetch with auth); documents, locations, contacts, polls and interactive button/list replies populate their own sub-objects. See Working with media.

Example: delivery receipt

{
  "id": "evt-7a1d4e8b",
  "event": "message.delivered",
  "timestamp": 1781827205,
  "device_id": "device-uuid",
  "data": {
    "message_ids": ["3EB0F8A1B2C3D4E5F6"],
    "chat_jid": "5511999999999@s.whatsapp.net",
    "receipt_type": "delivery",
    "message_count": 1,
    "is_group": false,
    "delivered_at": "2026-06-15T12:00:05Z"
  }
}

Example: connection event

{
  "id": "evt-9c0f2a31",
  "event": "device.connected",
  "timestamp": 1781827100,
  "device_id": "device-uuid",
  "data": {
    "device_id": "device-uuid",
    "status": "connected",
    "whatsapp_jid": "5511999999999@s.whatsapp.net",
    "is_connected": true,
    "is_logged_in": true
  }
}

Event catalog

Every subscribable event, grouped by category. The authoritative list is always GET /webhook/events.

CategoryEvents
Messagesmessage.received, message.sent, message.delivered, message.read, message.undecryptable, message.delete_for_me, mention.sent, media.retry
Connection & sessiondevice.connected, device.disconnected, device.logged_out, connection.pair_error, connection.stream_replaced, connection.failure, connection.temporary_ban, connection.manual_reconnect, connection.keepalive_timeout, connection.keepalive_restored, qr.generated, pair.success
Contacts & chatscontact.presence, contact.changed, contact.pushname, contact.about, contact.picture, chat.presence, chat.mark_as_read, chat.pin, chat.mute, chat.archive, chat.delete, chat.clear
Groupsgroup.info, group.joined (see Group management)
Callscall.offer, call.accept, call.pre_accept, call.reject, call.terminate, call.relay_latency, call.transport, call.unknown
Security & privacyidentity.change, privacy.blocklist, privacy.settings
Settings & labelssettings.unarchive_chats, settings.user_status_mute, label.edit, label.association_chat, label.association_message
Newslettersnewsletter.join, newsletter.leave, newsletter.live_update, newsletter.mute_change
Synchistory.sync, offline.sync_preview, offline.sync_completed, app_state.sync_complete

Verifying deliveries

When you set a secret, every callback includes a signature field. It is an HMAC-SHA256 of the JSON body keyed by your secret, prefixed with sha256=. Recompute it and compare in constant time:

const crypto = require("crypto");

function verify(payload, signature, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(JSON.stringify(payload)).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

If you do not configure a secret, the signature field is absent. In that case, protect your endpoint another way: serve it on an unguessable secret path, require a custom value via the headers option, and/or allowlist WUTS source IPs. Always use HTTPS so payloads are encrypted in transit.

Next steps

On this page