API v2 (bêta)
Webhooks
Un endpoint de webhook est une URL de votre serveur que WbizTool appelle lorsqu'il arrive quelque chose à un message : il a été envoyé, il a échoué, il a été annulé ou il a expiré. Vous êtes informé tout de suite au lieu d'interroger l'API en boucle, et chaque requête est signée pour que vous puissiez vérifier qu'elle vient de WbizTool.
| Opération | Endpoint | Scope |
|---|---|---|
| Ajouter un endpoint | POST /api/v2/webhooks | messages:read |
| Lister les endpoints | GET /api/v2/webhooks | messages:read |
| Obtenir un endpoint | GET /api/v2/webhooks/{id} | messages:read |
| Supprimer un endpoint | DELETE /api/v2/webhooks/{id} | messages:read |
| Remplacer le secret de signature | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| Envoyer un événement de test | POST /api/v2/webhooks/{id}/test | messages:read |
Les endpoints appartiennent à l'espace de travail et reçoivent les événements de tous ses messages, quelle que soit leur origine : l'API, le tableau de bord, les campagnes, les rappels ou les intégrations. C'est pourquoi une clé limitée à certains numéros ne peut pas les gérer : chaque appel répond 403 number_not_allowed.
Événements#
| Type | Quand |
|---|---|
message.sent | Un message est parti de votre numéro. |
message.failed | Un message n'a pas pu être envoyé ; error.message indique pourquoi. |
message.cancelled | Un message en attente a été annulé : avec l'API, depuis le tableau de bord, ou parce que son numéro ou sa campagne a été supprimé. |
message.expired | Un message n'a pas été envoyé avant son expires_at. |
ping | Un événement de test que vous avez demandé. |
Les événements pour les numéros (connecté, déconnecté) et pour les messages entrants viendront plus tard. Les coches de distribution et de lecture n'envoient pas d'événement ; obtenez le message pour les voir.
Ajouter un endpoint#
https://wbiztool.com/api/v2/webhooksCorps: JSON (application/json)
Corps
urlstringobligatoireVotre endpoint : une adresse
https://publique, jusqu'à 500 caractères. Les adresseshttp://, privées et locales (commelocalhostou192.168.x.x) sont refusées.eventsarray of stringsfacultatifLes types d'événements qu'il reçoit : n'importe lesquels parmi
message.sent,message.failed,message.cancelledetmessage.expired. Par défaut : les quatre.pingarrive toujours quand vous en demandez un.descriptionstringfacultatifUne note pour vous, jusqu'à 100 caractères, comme
Order system.
Un espace de travail peut avoir jusqu'à 5 endpoints. La réponse est 201 avec l'endpoint et son secret de signature, affiché cette seule fois. Conservez-le là où votre récepteur peut le lire, par exemple dans une variable d'environnement. Les réponses suivantes n'affichent que secret_last4 ; si vous perdez le secret, remplacez-le.
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'];Réponse (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"
}
Le vrai secret fait 64 caractères hexadécimaux. Une nouvelle tentative de cette requête avec la même Idempotency-Key n'ajoute pas de deuxième endpoint, et sa réponse omet le secret.
Erreurs à l'ajout d'un endpoint#
| HTTP | Code | Quand |
|---|---|---|
400 | validation_failed | url manque ou est trop long, events est vide ou contient un type inconnu, ou un champ est inconnu |
403 | webhook_limit_reached | L'espace de travail a déjà 5 endpoints |
403 | number_not_allowed | La clé est limitée à certains numéros |
422 | invalid_webhook_url | url n'est pas une adresse https:// publique |
Lister les endpoints#
https://wbiztool.com/api/v2/webhooksLes endpoints de l'espace de travail qui reçoivent des événements, les plus anciens d'abord, dans une seule liste data.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
La liste omet les endpoints arrêtés. Les propriétaires voient aussi les endpoints, et peuvent les retirer, sur Paramètres → Clés API sous Endpoints de webhook.
Obtenir un endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Un endpoint. last_delivery montre comment s'est passée sa dernière livraison. Un endpoint qui n'existe pas ou a été supprimé répond 404 webhook_not_found ; un endpoint arrêté est affiché avec active: false.
Supprimer un endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Plus rien ne lui est envoyé, y compris les nouvelles tentatives encore en attente. La réponse est {"id": 41, "deleted": true}.
Remplacer le secret de signature#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretCrée un nouveau secret pour l'endpoint. La réponse est 200 avec l'endpoint et son nouveau secret, affiché cette seule fois. L'ancien secret cesse immédiatement de fonctionner : les événements envoyés après cela sont signés avec le nouveau, y compris les nouvelles tentatives d'événements antérieurs. Mettez votre récepteur à jour tout de suite.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Envoyer un événement de test#
https://wbiztool.com/api/v2/webhooks/{id}/testMet en file d'attente un événement ping vers cet endpoint, signé comme tous les événements, pour que vous puissiez tester votre récepteur. La réponse est 202 :
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
Votre endpoint reçoit :
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
Le contenu d'un événement#
Chaque événement est un POST avec un corps 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
}
}
}
| Champ | Description |
|---|---|
id | L'identifiant de l'événement, evt_ suivi de 24 caractères. Un même événement garde son identifiant à chaque nouvelle tentative. |
type | L'un des types d'événements. |
created_at | Quand l'événement s'est produit. |
data.message | Pour les événements message.* : le Message tel qu'il était à ce moment-là. Un media.url qu'il contient fonctionne pendant au moins 26 heures. |
data.webhook_id | Pour ping : l'identifiant de l'endpoint. |
En-têtes :
| En-tête | Valeur |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | L'id de l'événement, le même à chaque nouvelle tentative |
WbizTool-Timestamp | Le moment où cette tentative a été envoyée, en secondes Unix, comme 1792310409 |
WbizTool-Signature | v1= suivi du HMAC-SHA256 en hexadécimal de <timestamp>.<raw body>, calculé avec le secret de l'endpoint |
Vérifier la signature#
Vérifiez chaque requête avant de lui faire confiance : calculez la signature à partir du corps brut (avant que votre framework n'analyse le JSON), comparez-la à l'en-tête en temps constant, et refusez les horodatages décalés de plus de 5 minutes, pour qu'une ancienne requête ne puisse pas être rejouée. C'est le même principe que les signatures des webhooks de l'API v1, avec le secret propre à l'endpoint.
Ensuite, utilisez WbizTool-Event-Id pour écarter les doublons : un même événement peut arriver plus d'une fois, par exemple quand votre réponse s'est perdue et que nous avons réessayé.
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 "", 200Avec Django, passez request.body et 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);Répondez 2xx rapidement, puis faites le travail lent ensuite (dans une file d'attente, par exemple).
Nouvelles tentatives#
- Répondez avec n'importe quel
2xxdans les 10 secondes. Tout le reste est un échec : un autre code de statut (4xxcompris), aucune réponse dans les 10 secondes, ou aucune connexion dans les 3 secondes. Les redirections sont suivies, 3 au maximum, et chacune doit mener à une adresse publique. - Une livraison échouée est retentée après 1, 5 et 15 minutes, puis 1, 2, 4, 8 et 8 heures : 9 essais sur environ 23 heures. Après le dernier, cet événement est abandonné.
- La nouvelle tentative a le même corps et le même
WbizTool-Event-Id, avec un nouveauWbizTool-Timestampet une nouvelle signature. - L'ordre n'est pas garanti. Un
message.sentretenté peut arriver après un événement ultérieur. Utilisez lestatusdu message, ou obtenez le message, quand l'ordre compte.
last_delivery sur l'endpoint montre comment s'est passée la dernière livraison : state (pending, delivered ou failed), le status_code qu'a répondu votre serveur, et quand l'événement a été créé.
Quand un endpoint s'arrête#
Un endpoint cesse de recevoir des événements dans l'un des cas suivants. Il reste dans GET /webhooks avec active: false et un disabled_reason, pour que vous puissiez voir pourquoi ; il peut encore être lu et supprimé, mais pas testé ni doté d'un nouveau secret (404).
- Il échoue depuis 24 heures. Si rien ne lui a été livré pendant 24 heures alors que ses livraisons continuaient d'échouer, il est désactivé et ses nouvelles tentatives en attente sont abandonnées. Paramètres → Clés API l'affiche alors comme Désactivé. Réparez votre serveur, puis ajoutez l'endpoint de nouveau.
- La clé qui l'a créé est révoquée ou expire. Révoquer une clé arrête tout ce qu'elle a mis en place. Ajoutez l'endpoint de nouveau avec une autre clé.
- Il est supprimé, avec Supprimer un endpoint ou sur Paramètres → Clés API.
Les endpoints arrêtés ne comptent pas dans les 5.
L'objet Endpoint de webhook#
| Champ | Type | Description |
|---|---|---|
id | integer | L'identifiant de l'endpoint. |
url | string | Où les événements sont envoyés. |
description | string | Votre note ; "" s'il n'y en a pas. |
events | array | Les types d'événements qu'il reçoit. |
secret | string | Le secret de signature : uniquement dans les réponses à Ajouter un endpoint et Remplacer le secret de signature. |
secret_last4 | string | Les 4 derniers caractères du secret, pour savoir lequel vous avez. |
active | boolean | false lorsqu'il ne reçoit plus d'événements – voir Quand un endpoint s'arrête. |
disabled_reason | string or null | Pourquoi il s'est arrêté (échecs pendant 24 heures, ou la clé qui l'a créé a été révoquée ou a expiré) ; null tant qu'il est actif. |
created_at | string | Quand il a été ajouté. |
last_delivery | object or null | La dernière livraison tentée : state, status_code (ou null quand votre serveur n'a pas répondu) et created_at. null avant la première. |
Voir aussi#
- Messages : l'objet Message dans
data.message - Signatures des webhooks de l'API v1
- Erreurs
