Passa al contenuto principale

UC-008 — Tracking della Consegna con Webhooks

CampoValore
IDUC-008
ObiettivoRegistrare un webhook, ricevere callback di delivery/read/inbound e gestire il ciclo di vita
CanaleTutti (SMS, RCS, WhatsApp)
Complessità⭐⭐ Intermedio
Tempo stimato20 minuti
API coinvoltePOST /api/partner-gateway/v1/webhooks/delivery-status, GET /webhooks/delivery-status, PUT /webhooks/delivery-status, DELETE /webhooks/delivery-status

Scenari reali​

  • LogisticaExpress — Dashboard real-time: Il pannello operativo mostra in tempo reale lo stato di migliaia di notifiche di spedizione. Ogni webhook aggiorna il contatore di consegnati/falliti.
  • AssicuraPlus — SLA monitoring: Il sistema misura il tempo tra invio e consegna per ogni canale, generando alert se il tempo supera la soglia contrattuale di 60 secondi.
  • FarmaOnline — Audit log: Ogni evento di delivery, lettura e risposta viene registrato nel database per compliance normativa e analisi post-campagna.

Flusso webhook​

Il diagramma illustra il ciclo completo: registri il webhook, invii un messaggio, il carrier consegna, e la tua app riceve la notifica in tempo reale.

Step 1 — Registra il webhook​

Registra il tuo endpoint HTTPS per ricevere le notifiche di delivery.

curl -X POST https://api.qlara.ai/api/partner-gateway/v1/webhooks/delivery-status \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"callbackUrl": "https://api.logisticaexpress.it/webhooks/delivery"
}'

Response — Webhook registrato​

L'API risponde 201 Created:

{
"companyId": 123,
"callbackUrl": "https://api.logisticaexpress.it/webhooks/delivery"
}
Un solo webhook attivo

È consentito un solo webhook di delivery-status per account, e riceve allo stesso modo gli eventi di SMS, RCS e WhatsApp. Se ne esiste già uno, la POST risponde 409 Conflict e lo lascia invariato: cambia la URL con PUT (Step 4), oppure eliminalo prima con DELETE (Step 5).

Verifica la configurazione attuale​

curl -X GET https://api.qlara.ai/api/partner-gateway/v1/webhooks/delivery-status \
-H "X-Api-Key: YOUR_API_KEY"
{
"companyId": 123,
"callbackUrl": "https://api.logisticaexpress.it/webhooks/delivery"
}

Se non è configurato alcun webhook, l'API risponde 404.

Step 2 — Invia un messaggio di test​

Invia un messaggio con simulation: true per validare la richiesta senza costi reali.

curl -X POST https://api.qlara.ai/api/message-server/whatsapp/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"destination": "+393401234567",
"phoneNumberId": 5,
"template": {
"id": 42
},
"placeholders": {
"nome": "Giulia"
},
"enableNotification": true,
"simulation": true,
"messageId": "test-webhook-001"
}'

Response — Messaggio di test accettato​

{
"messageId": "test-webhook-001",
"simulation": true,
"results": {
"whatsapp": {
"accepted": true
},
"rcs": null,
"sms": null
}
}

Un messaggio simulato viene validato ma mai inviato, quindi non produce alcuna callback. Una volta accettata la richiesta, inviala di nuovo senza simulation a un numero di tua proprietà per ricevere le callback dello Step 3.

Test locale con ngrok

Per testare i webhook in locale, usa ngrok per esporre il tuo server: ngrok http 3000. Registra la URL HTTPS generata come callback.

Step 3 — Ricevi le callback​

Il sistema invia una POST request al tuo callbackUrl quando arriva la ricevuta di consegna, quando il destinatario legge il messaggio e quando ti risponde. Ecco i payload per i tre principali tipi di evento. I campi senza valore vengono omessi.

Payload DELIVERY​

Notifica di consegna (o mancata consegna) del messaggio, inviata quando arriva la ricevuta del carrier o della piattaforma. Questa è relativa a un SMS:

{
"eventType": "DELIVERY",
"channel": "SMS",
"messageId": "shipment-48213",
"destination": "+393401234567",
"statusCode": 3,
"description": "delivered",
"eventDate": "2026-04-09T11:30:05+02:00",
"price": 0.035,
"totalParts": 1,
"numPart": 1
}

messageId è l'id del messaggio che hai inviato, lo stesso valore che l'API di polling chiama customerMessageId. Un SMS produce un evento per ogni parte (numPart di totalParts). Gli eventi DELIVERY di RCS e WhatsApp hanno gli stessi campi, senza totalParts e numPart.

Payload READ (solo WhatsApp/RCS)​

Notifica di lettura — il destinatario ha aperto il messaggio:

{
"eventType": "READ",
"channel": "WHATSAPP",
"messageId": "test-webhook-001",
"destination": "+393401234567",
"eventDate": "2026-04-09T11:31:15+02:00"
}

Un evento READ non ha statusCode: un messaggio letto resta DELIVERED.

Payload INBOUND (solo WhatsApp/RCS)​

Il destinatario ha risposto al messaggio:

{
"eventType": "INBOUND",
"channel": "WHATSAPP",
"messageId": "b7e2c9a4-3f1d-4c6e-8a5b-0d9f2e7c4a18",
"source": "+393401234567",
"destination": "+393209998877",
"receivedDate": "2026-04-09T11:35:00+02:00",
"messageType": "TEXT",
"text": "Si, confermo l'appuntamento di giovedi alle 15:30"
}

source è il numero dell'utente finale e destination il tuo numero WhatsApp (su RCS, l'id del tuo agente). messageId identifica il messaggio in arrivo nella conversazione. I messaggi multimediali includono anche un mediaKey.

Codici di stato​

Lo statusCode di un evento DELIVERY, con la sua description:

statusCodedescriptionCanaliSignificato
1acceptedSMSConsegnato al carrier, nessuna ricevuta ancora. Mai inviato come evento.
2rejectedSMSIl carrier ha rifiutato il messaggio.
3deliveredSMS, RCS, WhatsAppIl messaggio ha raggiunto il telefono.
4expiredSMS, RCS, WhatsAppNon consegnato entro il periodo di validità.
5deletedSMSAnnullato prima della consegna.
6undeliverableSMSLa destinazione non può riceverlo (numero irraggiungibile o non valido).
9general errorRCS, WhatsAppConsegna fallita.
10disabledRCS, WhatsAppIl destinatario ha il canale disattivato.
11unsupportedRCS, WhatsAppIl dispositivo o il numero non supporta il canale.
12conversation closedWhatsAppLa finestra di assistenza di 24 ore era chiusa.
0unknownSMS, RCS, WhatsAppNessuno stato disponibile per il messaggio. Mai inviato come evento.

Un evento viene inviato solo quando arriva la ricevuta, quindi ogni codice che ricevi è definitivo. Un messaggio ancora in transito risulta ACCEPTED su SMS e UNKNOWN su RCS e WhatsApp quando lo interroghi in polling, e non produce eventi. Gli stessi numeri corrispondono ai valori di deliveryStatus dell'API di polling: vedi Delivery Statuses.

Step 4 — Aggiorna il webhook​

Modifica la URL di callback senza eliminare e ricreare la configurazione.

curl -X PUT https://api.qlara.ai/api/partner-gateway/v1/webhooks/delivery-status \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"callbackUrl": "https://api.logisticaexpress.it/webhooks/v2/delivery"
}'

Response — Webhook aggiornato​

L'API risponde 200 OK (404 se non è configurato alcun webhook):

{
"companyId": 123,
"callbackUrl": "https://api.logisticaexpress.it/webhooks/v2/delivery"
}

Step 5 — Revoca il webhook​

Quando non hai più bisogno delle notifiche in tempo reale, elimina la configurazione.

curl -X DELETE https://api.qlara.ai/api/partner-gateway/v1/webhooks/delivery-status \
-H "X-Api-Key: YOUR_API_KEY"

Response — Webhook eliminato​

L'API risponde 204 No Content con corpo vuoto (404 se non è configurato alcun webhook).

Dopo la revoca, le notifiche di delivery non verranno più inviate. Potrai comunque consultare lo stato dei messaggi tramite polling.

Esempio — Webhook handler Node.js​

webhook-handler.js
const express = require('express');
const app = express();
app.use(express.json());

// Fallimenti definitivi: SMS = rejected, deleted, undeliverable
// RCS/WhatsApp = general error, disabled, unsupported, conversation closed
const SMS_FAILURES = [2, 5, 6];
const RCS_WA_FAILURES = [9, 10, 11, 12];

app.post('/webhooks/delivery', (req, res) => {
const { eventType, messageId, statusCode, channel, destination } = req.body;

// Un evento può arrivare più volte: deduplica su eventType + messageId (+ numPart per gli SMS)
// const eventKey = `${eventType}:${messageId}:${req.body.numPart ?? ''}`;
// if (db.events.exists(eventKey)) return res.sendStatus(200);

switch (eventType) {
case 'DELIVERY':
if (statusCode === 3) {
console.log(`[DELIVERED] ${messageId} via ${channel} a ${destination}`);
// Aggiorna il database: segna il messaggio come consegnato
// db.messages.update(messageId, { status: 'delivered', channel });
} else if (statusCode === 4) {
console.log(`[EXPIRED] ${messageId} via ${channel}`);
// Il messaggio è scaduto prima della consegna
} else if (SMS_FAILURES.includes(statusCode) || RCS_WA_FAILURES.includes(statusCode)) {
console.log(`[FAILED] ${messageId} via ${channel}: ${req.body.description}`);
// Gestisci il fallimento: riprova o notifica l'operatore
// alertService.notify(`Messaggio ${messageId} non consegnato: ${req.body.description}`);
}
break;

case 'READ':
console.log(`[READ] ${messageId} letto dal destinatario`);
// db.messages.update(messageId, { readAt: req.body.eventDate });
break;

case 'INBOUND':
console.log(`[INBOUND] Da ${req.body.source}: ${req.body.text}`);
// Processa la risposta: auto-reply, inoltra al CRM, ecc.
// crmService.createTicket(req.body.source, req.body.text, req.body.receivedDate);
break;

default:
console.log(`[UNKNOWN] Evento non gestito: ${eventType}`);
}

// Rispondi sempre velocemente (entro 5 secondi) - elabora in modo asincrono se necessario
res.sendStatus(200);
});

app.listen(3000, () => {
console.log('Webhook handler in ascolto sulla porta 3000');
});

Polling vs Webhook​

AspettoPollingWebhook
LatenzaDipende dalla frequenza di pollingQuasi real-time
ComplessitàSemplice (solo richieste GET)Richiede endpoint HTTPS pubblico
ScalabilitaAumenta le chiamate API a volume altoPush-based, nessuna chiamata extra
Costi APIOgni poll consuma una chiamataNessun consumo aggiuntivo
AffidabilitaNessun rischio di perdita eventiRichiede gestione retry e idempotenza
Ideale perBasso volume, controlli ad-hocAlto volume, dashboard real-time
Dietro le quinte — Meccanismo di delivery dei webhook

Il sistema di webhook funziona con un modello at-least-once delivery:

  1. Registrazione: Quando registri un webhook, il sistema valida la URL: https sulla porta 443 o 8443, nessuna credenziale, nessun frammento e un host che risolve a indirizzi pubblicamente raggiungibili. Una URL che non supera questi controlli viene rifiutata con 400. Alla URL non viene inviata alcuna richiesta di prova.
  2. Dispatch: Il sistema accoda un evento quando arriva la ricevuta del carrier o della piattaforma (DELIVERY, con il codice di stato definitivo), quando il destinatario legge un messaggio RCS o WhatsApp (READ) e quando un utente finale ti scrive su RCS o WhatsApp (INBOUND). Un messaggio ancora in transito (ACCEPTED su SMS, UNKNOWN su RCS e WhatsApp) non produce eventi.
  3. Retry: Se la tua callback URL non risponde 2xx entro 5 secondi (3 secondi per la connessione), il sistema ritenta: al massimo 5 tentativi in tutto, a distanza di almeno 5 minuti per gli SMS e di almeno 10 minuti per RCS e WhatsApp, e solo per i messaggi inviati negli ultimi 10 giorni.
  4. Ordinamento: Gli eventi possono arrivare più di una volta e in ordine sparso. Rendi l'elaborazione idempotente, usando come chiave eventType + messageId (+ numPart per gli SMS).

Best practices​

  1. Usa HTTPS — La callback URL deve usare HTTPS, sulla porta 443 o 8443, su un host pubblicamente raggiungibile.
  2. Rispondi rapidamente — Rispondi con un qualsiasi 2xx entro 5 secondi. Elabora il payload in modo asincrono se serve.
  3. Gestisci i retry — Il sistema ritenta in caso di errore, e un evento può arrivare più di una volta. Rendi l'elaborazione idempotente usando come chiave eventType + messageId (+ numPart per gli SMS).
  4. Valida il payload — Le callback non hanno un header di firma. Tratta il corpo come input non fidato e verifica che il messageId di un evento DELIVERY o READ appartenga a un messaggio che hai inviato.
  5. Monitora i fallimenti — Gli eventi che esauriscono i tentativi non vengono più inviati. Se il tuo endpoint è stato irraggiungibile, recupera gli stati mancanti in polling con GET /messages/status?channel=...&ids=....

Risultato atteso​

StepAzioneRisultato
1POST /webhooks/delivery-status201 Created con companyId e callbackUrl
2POST /whatsapp/send con simulation: true, poi senzaRichiesta validata, poi messaggio inviato
3Callback ricevutaPayload DELIVERY, READ o INBOUND
4PUT /webhooks/delivery-statusURL di callback aggiornata
5DELETE /webhooks/delivery-status204 No Content, notifiche interrotte

Prossimi passi​