Webhooks
Un unico endpoint firmato per account. Verifica della firma, catalogo degli eventi, policy di retry e rotazione del secret.
Indichi a WASync il Suo endpoint e riceve in cambio un secret di firma. Utilizza
whatsapp.events. Non è prevista alcuna registrazione dell'app né alcun passaggio di consenso.
Uno slot webhook per account
Un account ha esattamente un URL webhook. PUT /webhook lo imposta: se lo
richiama con un URL diverso, l'endpoint viene spostato invece di aggiungerne
un secondo, e non esiste alcuna eliminazione. Questo conta non appena più di un
componente vuole ricevere gli eventi: un trigger n8n e il
Suo backend non possono possedere entrambi lo slot. Distribuisca gli eventi dal Suo lato.
Impostare l'endpoint
curl -X PUT https://developers.wasync.app/api/v1/webhook \
-H "Authorization: Bearer $WASYNC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/wasync/webhook"}'{
"url": "https://example.com/wasync/webhook",
"events": [
"message.received",
"message.status",
"connection.disconnected",
"connection.connected"
],
"secret": "whsec_3f9a…"
}Conservi il secret. Viene rilasciato qui perché è il momento in cui Le
serve per scrivere il codice di verifica, e GET /webhook non lo restituirà
più: quell'endpoint riporta { url, events, secretSet }, volutamente senza il
secret, perché un percorso di lettura finisce nei log e nei ticket. Lo ha perso?
Chiami POST /webhook/rotate per ottenerne uno nuovo. L'URL deve essere HTTPS
su un host raggiungibile pubblicamente; http, localhost e gli intervalli
privati vengono rifiutati con 400 invalid_webhook.
La rotazione non prevede alcuna finestra di sovrapposizione: il vecchio secret smette di valere nell'istante in cui viene emesso il nuovo. Distribuisca subito il nuovo secret: la policy di retry qui sotto copre un rilascio che dura secondi, non ore.
Catalogo degli eventi
| Evento | Si attiva quando | Direzione |
|---|---|---|
message.received | Un messaggio WhatsApp arriva su una connessione autorizzata (testo o media). | Solo in entrata: i messaggi che invia Lei non lo attivano. |
message.status | Cambia lo stato di consegna di uno dei Suoi messaggi in uscita. | Solo in uscita. |
connection.disconnected | Una connessione ha smesso di funzionare e il suo titolare deve ripetere l'abbinamento. | Una volta per interruzione, non ripetutamente finché resta guasta. |
connection.connected | Una connessione che si era interrotta funziona di nuovo. | Una volta sola, e solo per una connessione di cui Le era stata segnalata l'interruzione. |
Ignori i valori event sconosciuti invece di trattarli come errori: i nuovi
tipi vengono annunciati nel changelog prima del rilascio.
Gli eventi vengono emessi solo per le connessioni qr. Le connessioni
cloud_api oggi non emettono webhook; per queste usi il polling su
GET /messages e GET /connections/{id}.
Payload
{
"event": "message.received",
"connection_id": "conn_8f3a21",
"message": {
"id": "cmqj3k2ab0001xyz",
"wa_id": "[email protected]_3EB0A1B2C3",
"from": "40700000000",
"text": "Hello!",
"media_url": null,
"media_type": null,
"timestamp": 1766138640000
}
}message.id: l'id stabile di WASync (lo stesso valore cheGET /messagesrestituisce comeid). Deduplichi e faccia i join su questo campo: la consegna è at-least-once.message.wa_id: l'id proprio di WhatsApp,nullquando non è noto. Solo per supporto e debug, mai come chiave di deduplicazione o di join.from: solo cifre in formato internazionale, senza+.textènullper i messaggi con solo media;media_url/media_typesononullper i messaggi di testo o quando il file non è scaricabile pubblicamente.timestamp: epoch in millisecondi come numero, non una stringa ISO.
Un evento message.status aggiorna uno dei Suoi messaggi in uscita:
{
"event": "message.status",
"connection_id": "conn_8f3a21",
"message": {
"id": "cmqj3k2ab0001xyz",
"wa_id": "[email protected]_3EB0A1B2C3",
"status": "read",
"timestamp": 1766138641000
}
}Verificare la firma
Ogni consegna riporta X-WASync-Timestamp (epoch ms) e
X-WASync-Signature: sha256=<hex>, che equivale a
HMAC-SHA256(timestamp + "." + rawBody, secret). La calcoli sul body raw,
prima di qualsiasi parsing JSON, e la confronti in tempo costante. Rifiuti le
consegne con un timestamp più vecchio di circa 5 minuti: è ciò che blocca i replay.
import crypto from "node:crypto";
// rawBody MUST be the exact bytes you received (verify BEFORE JSON.parse).
export function verifyWASync(headers, rawBody, secret) {
const ts = headers["x-wasync-timestamp"];
const sig = headers["x-wasync-signature"]; // "sha256=<hex>"
const expected =
"sha256=" +
crypto.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return Boolean(sig) &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}import express from "express";
import { verifyWASync } from "./verify.js";
const app = express();
// Capture the RAW body — signature verification needs the exact bytes.
app.post("/webhooks/wasync", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyWASync(req.headers, req.body.toString("utf8"), process.env.WASYNC_WEBHOOK_SECRET)) {
return res.status(401).end();
}
// Reject stale deliveries (replay window) — 5 minutes is a sane tolerance.
if (Math.abs(Date.now() - Number(req.headers["x-wasync-timestamp"])) > 5 * 60_000) {
return res.status(401).end();
}
res.status(200).end(); // ACK fast — under 10s — then process async
const { message } = JSON.parse(req.body.toString("utf8"));
// ... your logic (dedupe on message.id — retries can deliver duplicates)
});Consegna e policy di retry
| Proprietà | Valore |
|---|---|
| Criterio di successo | Qualsiasi 2xx. Tutto il resto, redirect inclusi, è un fallimento. |
| Tentativi | 3 per evento: subito, poi dopo 500 ms, poi dopo 1 s. |
| Timeout per tentativo | 10 secondi. Risponda subito con ACK ed elabori in modo asincrono. |
| Dopo l'ultimo fallimento | L'evento viene scartato. Non esiste una coda di riconsegna prolungata. |
| Duplicati | Possibili. Deduplichi su message.id. |
| Ordinamento | Non garantito. Se serve, ordini per timestamp. |
| Firma nei retry | Vengono rinviati lo stesso body e la stessa firma: verifichi ogni tentativo allo stesso modo. |
Scala degli stati
I valori di message.status formano una scala a senso unico: sent (1) →
delivered (2) → read (3), con failed come esito terminale che non segue read.
Applichi gli aggiornamenti in modo monotono. Applichi un nuovo stato solo se
il suo rango è superiore a quello che ha memorizzato. Un messaggio può arrivare
con read prima di delivered (una consegna tardiva) e sono possibili eventi
duplicati. Non riporti mai un messaggio a uno stato precedente nella Sua interfaccia.
Riconciliazione: il polling come rete di sicurezza
Le consegne vengono tentate tre volte e poi scartate: il Suo handler webhook è
quindi il percorso rapido, non la garanzia. Esegua un ciclo in background
che interroga GET /messages?connectionId=… ogni 30–60 secondi e inserisce
o aggiorna tutto ciò che l'handler non ha visto, usando il cursore per i
recuperi incrementali. Lo stesso ciclo recupera gli aggiornamenti di stato
arrivati mentre il Suo endpoint era fuori servizio.
Webhooks = aggiornamenti dell'interfaccia in tempo reale. Polling = correttezza. Usi entrambi.
Ciclo di vita della connessione
{
"event": "connection.disconnected",
"connection_id": "conn_8f2a…",
"connection": {
"id": "conn_8f2a…",
"phone_number": "393331234567",
"label": "Studio Rossi",
"status": "disconnected",
"needs_reconnect": true,
"license_status": "active",
"license_expires": "2027-08-08T09:00:00.000Z",
"reason": "AUTH_LOST"
},
"timestamp": 1786000000000
}Una connessione qr può smettere di funzionare per motivi indipendenti dalla
volontà di chiunque: il telefono resta spento per un giorno, WhatsApp viene
aperto su un altro dispositivo, la sessione scade. Il numero rimane allora
muto in entrambe le direzioni e, senza un evento, la prima persona ad
accorgersene è il Suo cliente.
Il codice QR non è volutamente incluso nel payload. Un QR di WhatsApp è una
credenziale di accesso tramite scansione: chiunque lo legga può prendere il
controllo della sessione. L'evento Le comunica che serve un nuovo abbinamento:
recuperi il QR con la Sua chiamata autenticata GET /connections/{id} e lo
mostri al titolare del numero, esattamente come durante la prima configurazione.
Basi la Sua interfaccia su needs_reconnect; consideri reason un semplice
suggerimento per i log, perché possono comparire nuovi valori.
Latenza di rilevamento: una disconnessione viene individuata da una verifica di stato ogni 5 minuti (~5 min). Quando la sessione sembra recuperabile, si tenta prima un ripristino automatico, quindi in quel caso il percorso richiede circa 15–20 minuti.
Non riceve gli eventi?
- L'URL di consegna è HTTPS su un host pubblico? Localhost, IP privati e
http://vengono rifiutati. Durante lo sviluppo usi un tunnel e verifichi cosa è effettivamente memorizzato conGET /webhook. - La credenziale include
whatsapp.eventse copre la connessione su cui è arrivato il messaggio? - La licenza della connessione è ancora attiva? Una licenza scaduta significa
nessun evento e
402sugli invii. - Il Suo endpoint risponde con
2xxentro 10 secondi? Gli handler lenti sembrano dei fallimenti e consumano tutti e tre i tentativi. - Si tratta di una connessione
qr? Le connessionicloud_apinon emettono webhook. - Solo i messaggi in entrata attivano
message.received: i Suoi invii non lo fanno mai.