API v2 (beta)
Webhook
Un endpoint del webhook è un URL sul tuo server che WbizTool chiama quando succede qualcosa a un messaggio: è stato inviato, non è riuscito, è stato annullato o è scaduto. Ricevi la notizia subito invece di dover interrogare l'API, e ogni richiesta è firmata, così puoi verificare che arrivi da WbizTool.
| Operazione | Endpoint | Scope |
|---|---|---|
| Aggiungere un endpoint | POST /api/v2/webhooks | messages:read |
| Elencare gli endpoint | GET /api/v2/webhooks | messages:read |
| Ottenere un endpoint | GET /api/v2/webhooks/{id} | messages:read |
| Rimuovere un endpoint | DELETE /api/v2/webhooks/{id} | messages:read |
| Sostituire il secret di firma | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| Inviare un evento di test | POST /api/v2/webhooks/{id}/test | messages:read |
Gli endpoint appartengono allo spazio di lavoro e ricevono gli eventi di tutti i suoi messaggi, chiunque li abbia inviati: l'API, la dashboard, le campagne, i promemoria o le integrazioni. Per questo una chiave limitata ad alcuni numeri non può gestirli: ogni chiamata risponde 403 number_not_allowed.
Eventi#
| Tipo | Quando |
|---|---|
message.sent | Un messaggio è partito dal tuo numero. |
message.failed | Non è stato possibile inviare un messaggio; error.message spiega perché. |
message.cancelled | Un messaggio in attesa è stato annullato: con l'API, dalla dashboard, o perché il suo numero o la sua campagna è stata rimossa. |
message.expired | Un messaggio non è stato inviato prima del suo expires_at. |
ping | Un evento di test che hai richiesto. |
Gli eventi per i numeri (collegato, disconnesso) e per i messaggi in arrivo arriveranno più avanti. Le spunte di consegna e di lettura non inviano eventi; ottieni il messaggio per vederle.
Aggiungere un endpoint#
https://wbiztool.com/api/v2/webhooksCorpo: JSON (application/json)
Body
urlstringobbligatorioIl tuo endpoint: un indirizzo
https://pubblico, fino a 500 caratteri. Gli indirizzihttp://, privati e locali (comelocalhosto192.168.x.x) vengono rifiutati.eventsarray of stringsfacoltativoI tipi di evento che riceve: uno qualsiasi tra
message.sent,message.failed,message.cancelledemessage.expired. Predefinito: tutti e quattro.pingarriva sempre quando ne richiedi uno.descriptionstringfacoltativoUna nota per te, fino a 100 caratteri, ad esempio
Order system.
Uno spazio di lavoro può avere fino a 5 endpoint. La risposta è 201 con l'endpoint e il suo secret di firma, mostrato solo questa volta. Conservalo dove il tuo ricevitore può leggerlo, ad esempio in una variabile d'ambiente. Le risposte successive mostrano solo secret_last4; se perdi il secret, sostituiscilo.
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'];Risposta (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"
}
Il vero secret è composto da 64 caratteri esadecimali. Un nuovo tentativo di questa richiesta con lo stesso Idempotency-Key non aggiunge un secondo endpoint, e la sua risposta omette il secret.
Errori durante l'aggiunta di un endpoint#
| HTTP | Codice | Quando |
|---|---|---|
400 | validation_failed | url manca o è troppo lungo, events è vuoto o contiene un tipo sconosciuto, oppure un campo è sconosciuto |
403 | webhook_limit_reached | Lo spazio di lavoro ha già 5 endpoint |
403 | number_not_allowed | La chiave è limitata ad alcuni numeri |
422 | invalid_webhook_url | url non è un indirizzo https:// pubblico |
Elencare gli endpoint#
https://wbiztool.com/api/v2/webhooksGli endpoint dello spazio di lavoro che ricevono eventi, dal più vecchio, in un'unica lista data.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
La lista omette gli endpoint che si sono fermati. Anche i Proprietari vedono gli endpoint, e possono rimuoverli, in Impostazioni → Chiavi API sotto Endpoint dei webhook.
Ottenere un endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Un singolo endpoint. last_delivery mostra com'è andata la sua consegna più recente. Un endpoint che non esiste o è stato rimosso risponde 404 webhook_not_found; uno fermato viene mostrato con active: false.
Rimuovere un endpoint#
https://wbiztool.com/api/v2/webhooks/{id}Non gli viene più inviato nulla, compresi i nuovi tentativi ancora in attesa. La risposta è {"id": 41, "deleted": true}.
Sostituire il secret di firma#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretCrea un nuovo secret per l'endpoint. La risposta è 200 con l'endpoint e il suo nuovo secret, mostrato solo questa volta. Il vecchio secret smette subito di funzionare: gli eventi inviati da questo momento sono firmati con quello nuovo, compresi i nuovi tentativi di eventi precedenti. Aggiorna subito il tuo ricevitore.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Inviare un evento di test#
https://wbiztool.com/api/v2/webhooks/{id}/testMette in coda un evento ping verso questo endpoint, firmato come ogni evento, così puoi provare il tuo ricevitore. La risposta è 202:
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
Il tuo endpoint riceve:
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
Il payload dell'evento#
Ogni evento è un POST con un body 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 | Descrizione |
|---|---|
id | L'id dell'evento, evt_ seguito da 24 caratteri. Lo stesso evento mantiene il suo id a ogni nuovo tentativo. |
type | Uno dei tipi di evento. |
created_at | Quando si è verificato l'evento. |
data.message | Per gli eventi message.*: il Message com'era in quel momento. Un media.url al suo interno funziona per almeno 26 ore. |
data.webhook_id | Per ping: l'id dell'endpoint. |
Header:
| Header | Valore |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | L'id dell'evento, uguale a ogni nuovo tentativo |
WbizTool-Timestamp | Quando è stato inviato questo tentativo, in secondi Unix, ad esempio 1792310409 |
WbizTool-Signature | v1= seguito dall'HMAC-SHA256 esadecimale di <timestamp>.<raw body>, calcolato con il secret dell'endpoint |
Verificare la firma#
Controlla ogni richiesta prima di fidarti: calcola la firma dal body grezzo (prima che il tuo framework analizzi il JSON), confrontala con l'header in tempo costante e rifiuta i timestamp che si discostano di più di 5 minuti, così una vecchia richiesta non può essere riprodotta. È lo stesso schema delle firme dei webhook dell'API v1, con il secret proprio dell'endpoint.
Poi usa WbizTool-Event-Id per scartare i duplicati: lo stesso evento può arrivare più di una volta, ad esempio quando la tua risposta è andata persa e abbiamo riprovato.
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 "", 200In Django passa 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);Rispondi subito con 2xx, poi esegui il lavoro lento in seguito (ad esempio in una coda).
Nuovi tentativi#
- Rispondi con un qualsiasi
2xxentro 10 secondi. Tutto il resto è un errore: un altro codice di stato (4xxcompreso), nessuna risposta entro 10 secondi o nessuna connessione entro 3. I reindirizzamenti vengono seguiti, al massimo 3, e ognuno deve portare a un indirizzo pubblico. - Una consegna non riuscita viene ritentata dopo 1, 5 e 15 minuti, poi dopo 1, 2, 4, 8 e 8 ore: 9 tentativi in circa 23 ore. Dopo l'ultimo, quell'evento viene scartato.
- Il nuovo tentativo ha lo stesso body e lo stesso
WbizTool-Event-Id, con un nuovoWbizTool-Timestampe una nuova firma. - L'ordine non è garantito. Un
message.sentritentato può arrivare dopo un evento successivo. Quando l'ordine conta, usa lostatusdel messaggio, oppure ottieni il messaggio.
last_delivery sull'endpoint mostra com'è andata la consegna più recente: state (pending, delivered o failed), lo status_code con cui ha risposto il tuo server e quando è stato creato l'evento.
Quando gli endpoint si fermano#
Un endpoint smette di ricevere eventi quando succede una di queste cose. Resta in GET /webhooks con active: false e un disabled_reason, così puoi vedere il motivo; si può ancora leggere e rimuovere, ma non provare né dotare di un nuovo secret (404).
- Non riesce da 24 ore. Se per 24 ore non gli è stato consegnato nulla mentre le sue consegne continuavano a non riuscire, viene disattivato e i suoi nuovi tentativi in attesa vengono scartati. Impostazioni → Chiavi API lo mostra allora come Disattivato. Sistema il tuo server, poi aggiungi di nuovo l'endpoint.
- La chiave che lo ha creato viene revocata o scade. Revocare una chiave ferma tutto ciò che ha configurato. Aggiungi di nuovo l'endpoint con un'altra chiave.
- Viene rimosso, con Rimuovere un endpoint o in Impostazioni → Chiavi API.
Gli endpoint fermati non contano nei 5.
L'oggetto endpoint del webhook#
| Campo | Tipo | Descrizione |
|---|---|---|
id | integer | L'id dell'endpoint. |
url | string | Dove vengono inviati gli eventi. |
description | string | La tua nota; "" se non ce n'è una. |
events | array | I tipi di evento che riceve. |
secret | string | Il secret di firma: solo nelle risposte di Aggiungere un endpoint e Sostituire il secret di firma. |
secret_last4 | string | Gli ultimi 4 caratteri del secret, per capire quale hai. |
active | boolean | false quando non riceve più eventi – vedi Quando gli endpoint si fermano. |
disabled_reason | string or null | Perché si è fermato (errori per 24 ore, oppure la chiave che lo ha creato è stata revocata o è scaduta); null mentre è attivo. |
created_at | string | Quando è stato aggiunto. |
last_delivery | object or null | L'ultima consegna tentata: state, status_code (o null quando il tuo server non ha risposto) e created_at. null prima della prima. |
Pagine correlate#
- Messaggi: l'oggetto Message in
data.message - Firme dei webhook per l'API v1
- Errori
