Webhooks

A webhook sends a session's events to your server as they happen: incoming messages, delivery receipts, status changes and more. Each session has its own webhook URL, event list and signing secret.

Set up

On the session's page open Webhook, enter your URL and tick the events you want. Or use a personal access token:

curl -X PUT https://api.warsita.com/v1/sessions/ses_3kV9mQ2pX7aL0zR5tN8w/webhook \
  -H "Authorization: Bearer $WARSITA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/warsita/webhook", "events": ["messages.received", "message-receipt.update", "session.status"]}'

The URL must be public and use https. The answer includes the signing secret (whsec_…); rotate it with POST /v1/sessions/{id}/webhook/secret. "url": "" turns the webhook off.

To try your endpoint, send a signed sample of any event with POST /v1/sessions/{id}/webhook/test and {"event": "messages.received"}, or use the simulator on the Webhook tab.

Requests

Every event is a POST with a JSON body:

{
  "event": "messages.received",
  "session_id": "ses_3kV9mQ2pX7aL0zR5tN8w",
  "timestamp": "2026-10-27T10:00:00.123Z",
  "data": { "…": "…" }
}

and these headers:

Header Value
X-Warsita-Event The event name.
X-Warsita-Delivery The delivery id (whd_…), the same on every retry. Use it to ignore duplicates.
X-Warsita-Signature t=<unix seconds>,v1=<signature>

Answer with any 2xx within 10 seconds. Anything else, or no answer, is retried after 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours. The session's delivery log, GET /v1/sessions/{id}/webhook/deliveries, shows the last 50 deliveries with their status code and error.

Verify the signature

v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with the webhook secret. Check it against the raw bytes before parsing the JSON, and reject old timestamps so a captured request cannot be replayed.

Node.js (Express):

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/warsita/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const parts = Object.fromEntries(req.get("X-Warsita-Signature").split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", process.env.WARSITA_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""))) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  // handle event.event and event.data
  res.sendStatus(200);
});

Python:

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts.get("v1", ""))

PHP:

function warsita_verify(string $secret, string $body, string $header): bool {
    parse_str(str_replace(",", "&", $header), $parts);
    $expected = hash_hmac("sha256", $parts["t"] . "." . $body, $secret);
    return abs(time() - (int) $parts["t"]) < 300 && hash_equals($expected, $parts["v1"] ?? "");
}

$ok = warsita_verify(getenv("WARSITA_WEBHOOK_SECRET"), file_get_contents("php://input"), $_SERVER["HTTP_X_WARSITA_SIGNATURE"]);

Events

Event When
messages.received A message arrived in any chat.
messages-personal.received A message arrived in a personal chat.
messages-group.received A message arrived in a group.
messages-newsletter.received A channel the number follows posted.
message.sent A message you sent through the API reached WhatsApp.
message-receipt.update Messages were delivered or read.
messages.update Someone edited a message.
messages.delete Someone deleted a message for everyone.
messages.reaction Someone reacted to a message.
session.status The session connected, disconnected, was logged out, banned or paused.
qrcode.updated A new pairing QR code is ready.
call Someone is calling the number.
groups.update A group's name, description or settings changed.
group-participants.update Members were added, removed, promoted or demoted.

messages.received covers every chat, and the three chat-specific events fire as well, so subscribe to one or the other. Messages sent from the number itself are not reported as received. The session settings ignore_groups, ignore_broadcasts and ignore_channels drop those chats' messages before they reach you.

Incoming message

data of the messages…received events:

{
  "id": "3EB0C431C26A1916E2A3",
  "chat": "6281234567890@s.whatsapp.net",
  "chat_type": "personal",
  "sender": "6281234567890@s.whatsapp.net",
  "sender_phone": "+6281234567890",
  "push_name": "Rina",
  "timestamp": "2026-10-27T10:00:00Z",
  "type": "image",
  "media": { "mimetype": "image/jpeg", "file_length": 48213, "caption": "My receipt" },
  "quoted_id": "3EB0A1B2C3D4E5F6A7B8"
}
Field
chat, chat_type Where it was sent: personal, group, newsletter or broadcast.
sender, sender_phone, push_name Who sent it. sender may be a privacy id (…@lid); sender_phone is their number when WhatsApp shares it, in groups too.
type text, image, video, audio, document, sticker, location, contact or other.
text The text of a text message.
media For media: mimetype, file_length, caption, filename, seconds, ptt. The file itself is not included.
location, contact For locations and contact cards.
mentions, quoted_id, quoted_sender, is_view_once Mentioned JIDs, the message it replies to, and whether it is view-once.

The API Reference lists the data of every other event.