API v2 (beta)
Webhooks
Um endpoint de webhook é uma URL no seu servidor que o WbizTool chama quando algo acontece com uma mensagem: ela foi enviada, falhou, foi cancelada ou expirou. Você recebe a notícia na hora em vez de consultar periodicamente, e cada requisição é assinada para você poder verificar que veio do WbizTool.
| Operação | Endpoint | Permissão |
|---|---|---|
| Adicionar um endpoint | POST /api/v2/webhooks | messages:read |
| Listar endpoints | GET /api/v2/webhooks | messages:read |
| Obter um endpoint | GET /api/v2/webhooks/{id} | messages:read |
| Remover um endpoint | DELETE /api/v2/webhooks/{id} | messages:read |
| Trocar o segredo de assinatura | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| Enviar um evento de teste | POST /api/v2/webhooks/{id}/test | messages:read |
Os endpoints pertencem ao espaço de trabalho e recebem eventos de todas as mensagens dele, seja quem for que as enviou: a API, o painel, campanhas, lembretes ou integrações. Por isso, uma chave limitada a alguns números não pode gerenciá-los: toda chamada responde 403 number_not_allowed.
Eventos#
| Tipo | Quando |
|---|---|
message.sent | Uma mensagem saiu do seu número. |
message.failed | Uma mensagem não pôde ser enviada; error.message diz o motivo. |
message.cancelled | Uma mensagem em espera foi cancelada: pela API, pelo painel ou porque o número ou a campanha dela foi removido. |
message.expired | Uma mensagem não foi enviada antes do seu expires_at. |
ping | Um evento de teste que você pediu. |
Eventos de números (conectado, desconectado) e de mensagens recebidas virão depois. Os tiques de entregue e lida não enviam eventos; obtenha a mensagem para vê-los.
Adicionar um endpoint#
https://wbiztool.com/api/v2/webhooksCorpo: JSON (application/json)
Corpo
urlstringobrigatórioO seu endpoint: um endereço público
https://, com até 500 caracteres. Endereçoshttp://, privados e locais (comolocalhostou192.168.x.x) são recusados.eventsarray of stringsopcionalOs tipos de evento que ele recebe: qualquer um de
message.sent,message.failed,message.cancelledemessage.expired. Padrão: os quatro.pingsempre chega quando você pede um.descriptionstringopcionalUma anotação para você, com até 100 caracteres, como
Order system.
Um espaço de trabalho pode ter até 5 endpoints. A resposta é 201 com o endpoint e o secret de assinatura, exibido só desta vez. Guarde-o onde o seu receptor possa lê-lo, como uma variável de ambiente. As respostas seguintes mostram só secret_last4; se você perder o segredo, troque-o.
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'];Resposta (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"
}
O secret real tem 64 caracteres hexadecimais. Uma nova tentativa desta requisição com a mesma Idempotency-Key não adiciona um segundo endpoint, e a resposta dela não traz o segredo.
Erros ao adicionar um endpoint#
| HTTP | Código | Quando |
|---|---|---|
400 | validation_failed | url está ausente ou é longa demais, events está vazio ou tem um tipo desconhecido, ou um campo é desconhecido |
403 | webhook_limit_reached | O espaço de trabalho já tem 5 endpoints |
403 | number_not_allowed | A chave está limitada a alguns números |
422 | invalid_webhook_url | url não é um endereço público https:// |
Listar endpoints#
https://wbiztool.com/api/v2/webhooksOs endpoints do espaço de trabalho que recebem eventos, do mais antigo para o mais recente, em uma única lista data.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
A lista deixa de fora os endpoints que pararam. Os Proprietários também veem os endpoints, e podem removê-los, em Configurações → Chaves de API, em Endpoints de webhook.
Obter um endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Um endpoint. last_delivery mostra como foi a última entrega. Um endpoint que não existe ou foi removido responde 404 webhook_not_found; um que parou aparece com active: false.
Remover um endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Nada mais é enviado a ele, nem as novas tentativas que ainda estavam em espera. A resposta é {"id": 41, "deleted": true}.
Trocar o segredo de assinatura#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretCria um novo segredo para o endpoint. A resposta é 200 com o endpoint e o novo secret, exibido só desta vez. O segredo antigo para de valer na hora: os eventos enviados depois disso são assinados com o novo, incluindo novas tentativas de eventos anteriores. Atualize o seu receptor imediatamente.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Enviar um evento de teste#
https://wbiztool.com/api/v2/webhooks/{id}/testColoca na fila um evento ping para este endpoint, assinado como todos os eventos, para você testar o seu receptor. A resposta é 202:
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
O seu endpoint recebe:
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
O conteúdo do evento#
Cada evento é um POST com um corpo 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
}
}
}
| Campo | Descrição |
|---|---|
id | O id do evento: evt_ e 24 caracteres. O mesmo evento mantém o id em todas as novas tentativas. |
type | Um dos tipos de evento. |
created_at | Quando o evento aconteceu. |
data.message | Nos eventos message.*: a Message como estava naquele momento. Um media.url nela funciona por pelo menos 26 horas. |
data.webhook_id | No ping: o id do endpoint. |
Cabeçalhos:
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | O id do evento, o mesmo em todas as novas tentativas |
WbizTool-Timestamp | Quando esta tentativa foi enviada, em segundos Unix, como 1792310409 |
WbizTool-Signature | v1= e o HMAC-SHA256 em hexadecimal de <timestamp>.<raw body>, feito com o segredo do endpoint |
Verificar a assinatura#
Verifique cada requisição antes de confiar nela: calcule a assinatura a partir do corpo bruto (antes de o seu framework interpretar o JSON), compare-a com o cabeçalho em tempo constante e recuse timestamps com mais de 5 minutos de diferença, para que uma requisição antiga não possa ser reenviada. É o mesmo esquema das assinaturas de webhook da API v1, com o segredo próprio do endpoint.
Depois, use WbizTool-Event-Id para descartar repetidos: o mesmo evento pode chegar mais de uma vez, por exemplo quando a sua resposta se perdeu e tentamos de novo.
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 "", 200No Django, passe request.body e 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);Responda 2xx rápido e faça o trabalho demorado depois (em uma fila, por exemplo).
Novas tentativas#
- Responda com qualquer
2xxem até 10 segundos. Qualquer outra coisa é uma falha: outro código de status (4xxincluído), nenhuma resposta em 10 segundos ou nenhuma conexão em 3. Redirecionamentos são seguidos, no máximo 3, e cada um precisa levar a um endereço público. - Uma entrega que falhou é tentada de novo após 1, 5 e 15 minutos, depois 1, 2, 4, 8 e 8 horas: 9 tentativas em cerca de 23 horas. Depois da última, o evento é descartado.
- A nova tentativa tem o mesmo corpo e o mesmo
WbizTool-Event-Id, com um novoWbizTool-Timestampe uma nova assinatura. - A ordem não é garantida. Um
message.sentreenviado pode chegar depois de um evento posterior. Use ostatusda mensagem, ou obtenha a mensagem, quando a ordem importar.
last_delivery no endpoint mostra como foi a última entrega: state (pending, delivered ou failed), o status_code com que o seu servidor respondeu e quando o evento foi criado.
Quando os endpoints param#
Um endpoint para de receber eventos quando acontece uma destas coisas. Ele continua em GET /webhooks com active: false e um disabled_reason, para você ver o motivo; ainda pode ser lido e removido, mas não testado nem receber um novo segredo (404).
- Ele falhou por 24 horas. Se nada foi entregue a ele em 24 horas enquanto as entregas continuavam falhando, ele é desativado e as novas tentativas em espera são descartadas. Configurações → Chaves de API passa a mostrá-lo como Desativado. Corrija o seu servidor e então adicione o endpoint de novo.
- A chave que o criou é revogada ou expira. Revogar uma chave interrompe tudo o que ela configurou. Adicione o endpoint de novo com outra chave.
- Ele é removido, com Remover um endpoint ou em Configurações → Chaves de API.
Endpoints que pararam não contam para os 5.
O objeto webhook endpoint#
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | O id do endpoint. |
url | string | Para onde os eventos são enviados. |
description | string | A sua anotação; "" quando não há nenhuma. |
events | array | Os tipos de evento que ele recebe. |
secret | string | O segredo de assinatura: só nas respostas de Adicionar um endpoint e Trocar o segredo de assinatura. |
secret_last4 | string | Os 4 últimos caracteres do segredo, para saber qual você tem. |
active | boolean | false quando ele não recebe mais eventos – veja Quando os endpoints param. |
disabled_reason | string or null | Por que ele parou (falhando por 24 horas, ou a chave que o criou foi revogada ou expirou); null enquanto ativo. |
created_at | string | Quando foi adicionado. |
last_delivery | object or null | A última entrega tentada: state, status_code (ou null quando o seu servidor não respondeu) e created_at. null antes da primeira. |
Relacionados#
- Mensagens: o objeto Message em
data.message - Assinaturas de webhook da API v1
- Erros
