UC-008 — Tracking della Consegna con Webhooks
| Campo | Valore |
|---|---|
| ID | UC-008 |
| Obiettivo | Registrare un webhook, ricevere callback di delivery/read/inbound e gestire il ciclo di vita |
| Canale | Tutti (SMS, RCS, WhatsApp) |
| Complessità | ⭐⭐ Intermedio |
| Tempo stimato | 20 minuti |
| API coinvolte | POST /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"
}
È 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.
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:
| statusCode | description | Canali | Significato |
|---|---|---|---|
1 | accepted | SMS | Consegnato al carrier, nessuna ricevuta ancora. Mai inviato come evento. |
2 | rejected | SMS | Il carrier ha rifiutato il messaggio. |
3 | delivered | SMS, RCS, WhatsApp | Il messaggio ha raggiunto il telefono. |
4 | expired | SMS, RCS, WhatsApp | Non consegnato entro il periodo di validità. |
5 | deleted | SMS | Annullato prima della consegna. |
6 | undeliverable | SMS | La destinazione non può riceverlo (numero irraggiungibile o non valido). |
9 | general error | RCS, WhatsApp | Consegna fallita. |
10 | disabled | RCS, WhatsApp | Il destinatario ha il canale disattivato. |
11 | unsupported | RCS, WhatsApp | Il dispositivo o il numero non supporta il canale. |
12 | conversation closed | La finestra di assistenza di 24 ore era chiusa. | |
0 | unknown | SMS, RCS, WhatsApp | Nessuno 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
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
| Aspetto | Polling | Webhook |
|---|---|---|
| Latenza | Dipende dalla frequenza di polling | Quasi real-time |
| Complessità | Semplice (solo richieste GET) | Richiede endpoint HTTPS pubblico |
| Scalabilita | Aumenta le chiamate API a volume alto | Push-based, nessuna chiamata extra |
| Costi API | Ogni poll consuma una chiamata | Nessun consumo aggiuntivo |
| Affidabilita | Nessun rischio di perdita eventi | Richiede gestione retry e idempotenza |
| Ideale per | Basso volume, controlli ad-hoc | Alto volume, dashboard real-time |
Dietro le quinte — Meccanismo di delivery dei webhook
Il sistema di webhook funziona con un modello at-least-once delivery:
- Registrazione: Quando registri un webhook, il sistema valida la URL:
httpssulla 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 con400. Alla URL non viene inviata alcuna richiesta di prova. - 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 (ACCEPTEDsu SMS,UNKNOWNsu RCS e WhatsApp) non produce eventi. - Retry: Se la tua callback URL non risponde
2xxentro 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. - Ordinamento: Gli eventi possono arrivare più di una volta e in ordine sparso. Rendi l'elaborazione idempotente, usando come chiave
eventType+messageId(+numPartper gli SMS).
Best practices
- Usa HTTPS — La callback URL deve usare HTTPS, sulla porta 443 o 8443, su un host pubblicamente raggiungibile.
- Rispondi rapidamente — Rispondi con un qualsiasi
2xxentro 5 secondi. Elabora il payload in modo asincrono se serve. - 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(+numPartper gli SMS). - Valida il payload — Le callback non hanno un header di firma. Tratta il corpo come input non fidato e verifica che il
messageIddi un evento DELIVERY o READ appartenga a un messaggio che hai inviato. - 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
| Step | Azione | Risultato |
|---|---|---|
| 1 | POST /webhooks/delivery-status | 201 Created con companyId e callbackUrl |
| 2 | POST /whatsapp/send con simulation: true, poi senza | Richiesta validata, poi messaggio inviato |
| 3 | Callback ricevuta | Payload DELIVERY, READ o INBOUND |
| 4 | PUT /webhooks/delivery-status | URL di callback aggiornata |
| 5 | DELETE /webhooks/delivery-status | 204 No Content, notifiche interrotte |
Prossimi passi
- Guida Webhooks: Approfondisci configurazione, testing locale e best practices
- UC-005 — Multi-Channel Fallback: Combina fallback multi-canale con tracking webhook
- Panoramica canali: Scopri quali eventi sono disponibili per ogni canale