What's new, 21 October 2026: A customer who sends several messages before the bot's first reply no longer gets the opening sequence of a Chat Flow twice
WASync · Developers

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

PUT /webhook
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"}'
200 response
{
  "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

EventoSi attiva quandoDirezione
message.receivedUn messaggio WhatsApp arriva su una connessione autorizzata (testo o media).Solo in entrata: i messaggi che invia Lei non lo attivano.
message.statusCambia lo stato di consegna di uno dei Suoi messaggi in uscita.Solo in uscita.
connection.disconnectedUna connessione ha smesso di funzionare e il suo titolare deve ripetere l'abbinamento.Una volta per interruzione, non ripetutamente finché resta guasta.
connection.connectedUna 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

POST your webhook URL
{
  "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 che GET /messages restituisce come id). Deduplichi e faccia i join su questo campo: la consegna è at-least-once.
  • message.wa_id: l'id proprio di WhatsApp, null quando non è noto. Solo per supporto e debug, mai come chiave di deduplicazione o di join.
  • from: solo cifre in formato internazionale, senza +.
  • text è null per i messaggi con solo media; media_url / media_type sono null per 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:

POST your webhook URL
{
  "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.

verify.js
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));
}
receiver (express)
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 successoQualsiasi 2xx. Tutto il resto, redirect inclusi, è un fallimento.
Tentativi3 per evento: subito, poi dopo 500 ms, poi dopo 1 s.
Timeout per tentativo10 secondi. Risponda subito con ACK ed elabori in modo asincrono.
Dopo l'ultimo fallimentoL'evento viene scartato. Non esiste una coda di riconsegna prolungata.
DuplicatiPossibili. Deduplichi su message.id.
OrdinamentoNon garantito. Se serve, ordini per timestamp.
Firma nei retryVengono 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

POST your webhook URL
{
  "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 con GET /webhook.
  • La credenziale include whatsapp.events e copre la connessione su cui è arrivato il messaggio?
  • La licenza della connessione è ancora attiva? Una licenza scaduta significa nessun evento e 402 sugli invii.
  • Il Suo endpoint risponde con 2xx entro 10 secondi? Gli handler lenti sembrano dei fallimenti e consumano tutti e tre i tentativi.
  • Si tratta di una connessione qr? Le connessioni cloud_api non emettono webhook.
  • Solo i messaggi in entrata attivano message.received: i Suoi invii non lo fanno mai.
Questa pagina è stata utile?

On this page