Webhooks

Webhooks

Endpoints, events, signature, and retries.

Webhooks — managing endpoints

GET    /instances/{instance_id}/webhooks
POST   /instances/{instance_id}/webhooks
PATCH  /instances/{instance_id}/webhooks/{webhook_id}
POST   /instances/{instance_id}/webhooks/{webhook_id}/enabled   { "enabled": true }
DELETE /instances/{instance_id}/webhooks/{webhook_id}

POST /instances/{instance_id}/webhooks
{
  "destination_url": "https://your-site.com/webhook",
  "events": ["message", "message.ack", "session.status", "message.unknown"],
  "client_sign_secret": "your-secret",
  "ignore_status": true
}
→ 201 { "success": true, "webhook_id": "whk_…" }

Webhooks are created disabled — enable via the enabled path. PATCH replaces the whole config, so always send destination_url and events; an empty client_sign_secret keeps the current secret and clear_client_sign = true removes it. All of this is also available on the session page.

Webhooks — events

session.statusSession status changed (WORKING, SCAN_QR_CODE, FAILED…) with the linked phone.
messageIncoming message from others.
message.anyEvery message in the session, incoming and outgoing (fromMe).
message.ackDelivery/read update for a message you sent (ack field).
message.reactionReaction (emoji) on a message.
message.editedMessage edited.
message.revokedMessage deleted for everyone.
message.unknownPlatform event: a message you sent via the API became unknown and needs your decision.

More events are available (groups, calls, labels, polls, presence) — see the full list in the webhook form on the session page.

ignore_status (default true): WhatsApp Status/Stories (status@broadcast) are not sent to you. Set false to receive them with message events.

Webhooks — request sent to your endpoint

POST https://your-site.com/webhook
Content-Type: application/json
X-TProTool-Event-Id: evt_…
X-TProTool-Timestamp: 1760209200
X-TProTool-Signature: v1=…

{
  "event_id": "evt_…",
  "event": "message",
  "instance_id": "ins_…",
  "timestamp": 1760209200000,
  "payload": {
    "id": "…",
    "from": "[email protected]",
    "from_phone": "9665xxxxxxxx",
    "fromMe": false,
    "body": "Hello",
    "hasMedia": true,
    "media": {
      "status": "ready",
      "mimetype": "image/jpeg",
      "media_id": "med_…",
      "url": "https://…/m/…",
      "expires_at": "…"
    }
  }
}

// message.unknown
{
  "event_id": "evt_…",
  "event": "message.unknown",
  "instance_id": "ins_…",
  "payload": { "message_id": "msg_…", "client_reference": "order-123", "to": "9665xxxxxxxx", "type": "text", "error_code": "TIMEOUT" }
}
// error_code: TIMEOUT | WORKER_DIED
media.status = readyFile stored — download from url before expires_at.
media.status = too_largeFile exceeds the allowed size and was not stored.
media.status = failedCould not download the file from WhatsApp.
media.status = outgoingMedia in a message sent by the session — no copy is stored.
media.status = unavailableMedia is not available for download.

Webhooks — verifying the signature

When client_sign_secret is set, the X-TProTool-Signature header carries v1= followed by HMAC-SHA256 of (timestamp + "." + raw request body) with your secret. X-TProTool-Event-Id and X-TProTool-Timestamp are always sent. Reject requests whose signature does not match or whose timestamp is older than 5 minutes.

<?php
$secret = 'your-secret';
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_TPROTOOL_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_TPROTOOL_SIGNATURE'] ?? '';

$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// ignore if $event['event_id'] was already processed
http_response_code(200);

Webhooks — responses and retries

Reply with any 2xx within 8 seconds for a successful delivery. Otherwise delivery is retried with growing intervals (5, 10, 20, 40, 80 seconds) up to 6 attempts, then marked as failed permanently; you can redeliver it manually from the delivery log on the session page.

410 Gone: the webhook is disabled and nothing more is sent until you enable it again. 429 with Retry-After: the attempt is postponed to the requested time (up to one hour).

event_id stays the same for an event across all attempts — store it and ignore events you have already received. Reply fast and process afterwards if handling takes long.