API v2 (бета)
Вебхуки
Webhook endpoint — это URL на вашем сервере, который WbizTool вызывает, когда с сообщением что-то происходит: оно отправлено, не отправлено, отменено или истекло. Вы узнаёте об этом сразу, без опроса, а каждый запрос подписан, поэтому можно убедиться, что он пришёл от WbizTool.
| Операция | Endpoint | Область доступа |
|---|---|---|
| Добавление endpoint | POST /api/v2/webhooks | messages:read |
| Список endpoint | GET /api/v2/webhooks | messages:read |
| Получение endpoint | GET /api/v2/webhooks/{id} | messages:read |
| Удаление endpoint | DELETE /api/v2/webhooks/{id} | messages:read |
| Замена секрета подписи | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| Отправка тестового события | POST /api/v2/webhooks/{id}/test | messages:read |
Endpoint принадлежат рабочему пространству и получают события по всем его сообщениям, откуда бы они ни были отправлены: через API, из панели, кампаниями, напоминаниями или интеграциями. Поэтому ключ, ограниченный некоторыми номерами, не может ими управлять: каждый вызов возвращает 403 number_not_allowed.
События#
| Тип | Когда |
|---|---|
message.sent | Сообщение ушло с вашего номера. |
message.failed | Сообщение не удалось отправить; причина — в error.message. |
message.cancelled | Ожидающее сообщение отменено: через API, в панели или потому, что его номер или кампания были удалены. |
message.expired | Сообщение не было отправлено до своего expires_at. |
ping | Запрошенное вами тестовое событие. |
События для номеров (подключён, вышел из аккаунта) и для входящих сообщений появятся позже. Галочки «доставлено» и «прочитано» событий не отправляют; чтобы их увидеть, получите сообщение.
Добавление endpoint#
https://wbiztool.com/api/v2/webhooksТело запроса: JSON (application/json)
Тело запроса
urlstringобязательноВаш endpoint: публичный адрес
https://, до 500 символов. Адресаhttp://, частные и локальные адреса (например,localhostили192.168.x.x) отклоняются.eventsarray of stringsнеобязательноТипы событий, которые он получает: любые из
message.sent,message.failed,message.cancelledиmessage.expired. По умолчанию — все четыре.pingприходит всегда, когда вы его запрашиваете.descriptionstringнеобязательноЗаметка для себя, до 100 символов, например
Order system.
В рабочем пространстве может быть до 5 endpoint. Ответ — 201 с endpoint и его secret подписи, который показывается только этот раз. Сохраните его там, где его сможет прочитать ваш обработчик, например в переменной окружения. Дальнейшие ответы показывают только secret_last4; если вы потеряете секрет, замените его.
curl -X POST https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system"
}'import os
import requests
response = requests.post(
"https://wbiztool.com/api/v2/webhooks",
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
json={
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system",
},
timeout=30,
)
endpoint = response.json()
print("Store this secret now:", endpoint["secret"])// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/webhooks", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/wbiztool/events",
events: ["message.sent", "message.failed"],
description: "Order system",
}),
});
const endpoint = await response.json();
console.log("Store this secret now:", endpoint.secret);<?php
$ch = curl_init('https://wbiztool.com/api/v2/webhooks');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/wbiztool/events',
'events' => ['message.sent', 'message.failed'],
'description' => 'Order system',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$endpoint = json_decode(curl_exec($ch), true);
curl_close($ch);
echo 'Store this secret now: ' . $endpoint['secret'];Ответ (201):
{
"id": 41,
"url": "https://example.com/wbiztool/events",
"description": "Order system",
"events": ["message.sent", "message.failed"],
"secret_last4": "9c1e",
"active": true,
"disabled_reason": null,
"created_at": "2026-10-18T11:20:05.733Z",
"last_delivery": null,
"secret": "3f0b…9c1e"
}
Настоящий secret состоит из 64 шестнадцатеричных символов. Повтор этого запроса с тем же Idempotency-Key не добавляет второй endpoint, а в его ответе секрета нет.
Ошибки при добавлении endpoint#
| HTTP | Код | Когда |
|---|---|---|
400 | validation_failed | url отсутствует или слишком длинный, events пуст или содержит неизвестный тип, или поле неизвестно |
403 | webhook_limit_reached | В рабочем пространстве уже 5 endpoint |
403 | number_not_allowed | Ключ ограничен некоторыми номерами |
422 | invalid_webhook_url | url — не публичный адрес https:// |
Список endpoint#
https://wbiztool.com/api/v2/webhooksEndpoint рабочего пространства, которые получают события, сначала самые старые, одним списком data.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
В список не входят endpoint, которые перестали работать. Владельцы тоже видят endpoint и могут их удалять на странице Настройки → API-ключи в разделе Адреса вебхуков.
Получение endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Один endpoint. last_delivery показывает, как прошла его последняя доставка. Endpoint, которого не существует или который удалён, возвращает 404 webhook_not_found; переставший работать показывается с active: false.
Удаление endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Ему больше ничего не отправляется, включая ещё ожидающие повторы. Ответ — {"id": 41, "deleted": true}.
Замена секрета подписи#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretСоздаёт для endpoint новый секрет. Ответ — 200 с endpoint и его новым secret, который показывается только этот раз. Старый секрет сразу перестаёт действовать: события, отправленные после этого, подписываются новым, включая повторы более ранних событий. Сразу обновите свой обработчик.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Отправка тестового события#
https://wbiztool.com/api/v2/webhooks/{id}/testСтавит в очередь событие ping для этого endpoint, подписанное, как любое событие, чтобы вы могли проверить свой обработчик. Ответ — 202:
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
Ваш endpoint получает:
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
Содержимое события#
Каждое событие — это POST с телом JSON:
{
"id": "evt_c41d7e2a9f3b8c6d5e4f3a21",
"type": "message.sent",
"created_at": "2026-10-18T10:15:09.912Z",
"data": {
"message": {
"id": 9817263,
"status": "sent",
"from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
"to": { "phone": "+919876543210", "contact_id": 991 },
"text": "Hi Asha, your order #1042 is ready.",
"media": null,
"send_at": null,
"expires_at": null,
"created_at": "2026-10-18T10:15:02.311Z",
"sent_at": "2026-10-18T10:15:09.870Z",
"error": null
}
}
}
| Поле | Описание |
|---|---|
id | ID события: evt_ и 24 символа. При каждом повторе у события тот же ID. |
type | Один из типов событий. |
created_at | Когда произошло событие. |
data.message | Для событий message.*: Message в том виде, каким оно было тогда. media.url в нём работает не меньше 26 часов. |
data.webhook_id | Для ping: ID endpoint. |
Заголовки:
| Заголовок | Значение |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | id события, одинаковый при каждом повторе |
WbizTool-Timestamp | Когда отправлена эта попытка, в секундах Unix, например 1792310409 |
WbizTool-Signature | v1= и шестнадцатеричный HMAC-SHA256 от <timestamp>.<raw body>, вычисленный секретом endpoint |
Проверка подписи#
Проверяйте каждый запрос, прежде чем ему доверять: вычислите подпись от необработанного тела (до того, как ваш фреймворк разберёт JSON), сравните её с заголовком за постоянное время и отклоняйте метки времени, расходящиеся больше чем на 5 минут, чтобы старый запрос нельзя было отправить повторно. Это та же схема, что у подписей вебхуков API v1, только с собственным секретом endpoint.
Затем используйте WbizTool-Event-Id, чтобы отбрасывать повторы: одно и то же событие может прийти больше одного раза, например когда ваш ответ потерялся и мы попробовали снова.
import hashlib, hmac, json, os, time
SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"]
def verify(secret: str, body: bytes, headers) -> bool:
ts, sig = headers.get("WbizTool-Timestamp", ""), headers.get("WbizTool-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
mac = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
# Flask
from flask import Flask, request
app = Flask(__name__)
seen = set() # use your database in production
@app.post("/wbiztool/events")
def events():
body = request.get_data() # the raw bytes
if not verify(SECRET, body, request.headers):
return "", 401
event_id = request.headers["WbizTool-Event-Id"]
if event_id in seen:
return "", 200 # a repeat: already handled
seen.add(event_id)
event = json.loads(body)
if event["type"] == "message.failed":
message = event["data"]["message"]
print("Failed:", message["id"], message["error"]["message"])
return "", 200В Django передавайте request.body и request.headers.
const crypto = require("crypto");
const express = require("express");
const SECRET = process.env.WBIZTOOL_WEBHOOK_SECRET;
function verify(secret, rawBody, headers) {
const ts = String(headers["wbiztool-timestamp"] || ""), sig = String(headers["wbiztool-signature"] || "");
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = "v1=" + crypto.createHmac("sha256", secret).update(ts + ".").update(rawBody).digest("hex");
return sig.length === mac.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mac));
}
const app = express();
const seen = new Set(); // use your database in production
// express.raw keeps the body as a Buffer: the signature covers the exact bytes.
app.post("/wbiztool/events", express.raw({ type: "*/*" }), (req, res) => {
if (!verify(SECRET, req.body, req.headers)) return res.sendStatus(401);
const eventId = req.headers["wbiztool-event-id"];
if (seen.has(eventId)) return res.sendStatus(200); // a repeat: already handled
seen.add(eventId);
const event = JSON.parse(req.body);
if (event.type === "message.failed") {
const message = event.data.message;
console.log("Failed:", message.id, message.error.message);
}
res.sendStatus(200);
});
app.listen(3000);Отвечайте 2xx быстро, а медленную работу выполняйте потом (например, в очереди).
Повторные попытки#
- Ответьте любым
2xxв течение 10 секунд. Всё остальное — неудача: другой код состояния (включая4xx), отсутствие ответа в течение 10 секунд или отсутствие соединения в течение 3 секунд. Перенаправления выполняются, не больше 3, и каждое должно вести на публичный адрес. - Неудачная доставка повторяется через 1, 5 и 15 минут, затем через 1, 2, 4, 8 и 8 часов: 9 попыток примерно за 23 часа. После последней это событие отбрасывается.
- У повтора то же тело и тот же
WbizTool-Event-Id, но новыеWbizTool-Timestampи подпись. - Порядок не гарантируется. Повторённое
message.sentможет прийти после более позднего события. Когда порядок важен, используйтеstatusсообщения или получите сообщение.
last_delivery у endpoint показывает, как прошла последняя доставка: state (pending, delivered или failed), status_code, которым ответил ваш сервер, и когда было создано событие.
Когда endpoint перестаёт работать#
Endpoint перестаёт получать события в одном из этих случаев. Он остаётся в GET /webhooks с active: false и disabled_reason, чтобы вы видели причину; его по-прежнему можно прочитать и удалить, но нельзя протестировать или выдать ему новый секрет (404).
- Он не работает 24 часа. Если за 24 часа ему ничего не было доставлено, а доставки продолжали завершаться неудачей, он отключается, а его ожидающие повторы отбрасываются. На странице «Настройки → API-ключи» он тогда показан как Отключён. Исправьте свой сервер, а затем добавьте endpoint снова.
- Ключ, который его создал, отозван или истёк. Отзыв ключа останавливает всё, что он настроил. Добавьте endpoint снова другим ключом.
- Он удалён через Удаление endpoint или на странице «Настройки → API-ключи».
Переставшие работать endpoint не учитываются в лимите 5.
Объект webhook endpoint#
| Поле | Тип | Описание |
|---|---|---|
id | integer | ID endpoint. |
url | string | Куда отправляются события. |
description | string | Ваша заметка; "", если её нет. |
events | array | Типы событий, которые он получает. |
secret | string | Секрет подписи: только в ответах на Добавление endpoint и Замену секрета подписи. |
secret_last4 | string | Последние 4 символа секрета, чтобы понять, какой из них у вас. |
active | boolean | false, когда он больше не получает события, — см. Когда endpoint перестаёт работать. |
disabled_reason | string or null | Почему он перестал работать (неудачи в течение 24 часов или ключ, который его создал, отозван или истёк); null, пока он активен. |
created_at | string | Когда он был добавлен. |
last_delivery | object or null | Последняя попытка доставки: state, status_code (или null, если ваш сервер не ответил) и created_at. null до первой. |
Связанные материалы#
- Сообщения: объект Message в
data.message - Подписи вебхуков для API v1
- Ошибки
