Webhook
Contratto completo dei webhook Blu Pantheon: registrazione, header, envelope, catalogo eventi, ritentativi, dead letter e test locale.
I webhook sono il canale outbound della piattaforma: quando accade un evento di business,
Blu Pantheon esegue una POST HTTPS firmata verso l'endpoint che hai registrato.
Prima di iniziare
- Un endpoint HTTPS pubblico con certificato TLS valido
- Permesso identity:webhooks:write sul tenant
- Un archivio per la deduplica delle consegne (tabella o cache con TTL di 7 giorni)
Requisiti dell'endpoint
| Requisito | Dettaglio | Se non rispettato |
|---|---|---|
| Protocollo | Solo https://. HTTP in chiaro è rifiutato in fase di registrazione. | Registrazione bloccata |
| Certificato TLS | Valido, non scaduto, catena completa, hostname corrispondente. | Consegna fallita, nessun ritentativo utile |
| Raggiungibilità | Pubblica da Internet. Nessuna VPN, nessuna autenticazione di rete. | Timeout di connessione |
| Metodo | Deve accettare POST con Content-Type: application/json. | 405 e consegna fallita |
| Risposta | Un qualsiasi 2xx entro 10 secondi. Consigliato 202. | Ritentativo |
| Redirect | Nessuno. 301, 302, 307, 308 non vengono seguiti. | Consegna fallita |
| Dimensione risposta | Corpo di risposta ignorato, mantienilo vuoto o minimo. | Nessun impatto |
| Autenticazione applicativa | Non usare Basic Auth o header custom: l'autenticità si verifica con la firma HMAC. | 401 sul tuo endpoint |
Perché rispondere 202
202 Accepted comunica correttamente la semantica: hai preso in carico il messaggio, lo
elaborerai in modo asincrono. Consente di rispondere in pochi millisecondi anche quando il
sistema a valle è lento.
Registrare un endpoint
Apri la sezione Webhook
Dal menu Configurazione seleziona Webhook. La tabella mostra gli endpoint registrati con stato, eventi sottoscritti e percentuale di consegne riuscite nelle ultime 24 ore.
Inserisci i dati dell'endpoint
| Campo | Valore |
|---|---|
| Nome | Automazione ACME — produzione |
| URL | https://automation.acme.it/webhooks/blupantheon |
| Progetto | bp_proj_7f3a9c21 |
| Stato | Attivo |
| Versione API | 2026-09-01 |
Seleziona gli eventi
Sottoscrivi solo gli eventi che il tuo flusso gestisce. Una sottoscrizione totale genera traffico inutile e rende difficile la diagnosi.
Copia il segreto di firma
Alla creazione viene generato il segreto. Copialo subito: non sarà più visibile.
Segreto di firma
whsec_xx••••••••••••••Usato per calcolare e verificare X-BP-Signature. Conservalo nel gestore di segreti.
Invia un evento di prova
Usa il pulsante Invia evento di prova e scegli webhook.test. Verifica nei
log consegne che la risposta sia 2xx.
Header di ogni consegna
Header HTTP inviati da Blu Pantheon
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
X-BP-Signature | string | Sì | — | Firma HMAC-SHA256 nel formato "t=1767225600,v1=<hex>". Può contenere più valori v1 durante una rotazione. |
X-BP-Event | string | Sì | — | Tipo di evento, es. "session.handover.requested". Coincide con il campo event del payload. |
X-BP-Delivery | string (uuid) | Sì | — | Identificativo univoco della consegna. Costante tra i ritentativi. Usalo per la deduplica. |
X-BP-Timestamp | string (unix seconds) | Sì | — | Istante della firma in secondi Unix. Deve rientrare nella tolleranza di 5 minuti. |
X-BP-Attempt | string (numero) | Sì | — | Numero del tentativo, da 1 a 5. Utile per distinguere una prima consegna da un ritentativo. |
X-BP-Api-Version | string | Sì | — | Versione del contratto del payload, es. "2026-09-01". |
Content-Type | string | Sì | — | Sempre "application/json; charset=utf-8". |
User-Agent | string | Sì | — | Sempre "BluPantheon-Webhooks/1.0". Non usarlo come meccanismo di autenticazione. |
Envelope comune
Tutti i payload condividono la stessa struttura esterna. I dati specifici dell'evento stanno in
data.
{
"id": "evt_01J9X4K2R7M3Q8YV5T0B6D2WQ",
"event": "session.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:20:00.412Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {}
}| Campo | Tipo | Descrizione |
|---|---|---|
id | string | Identificativo dell'evento. Stabile tra i ritentativi. |
event | string | Tipo di evento, coincide con X-BP-Event. |
apiVersion | string | Versione del contratto del payload. |
createdAt | string ISO 8601 UTC | Istante in cui l'evento è stato generato. Usalo per l'ordinamento. |
tenantId | string | Tenant proprietario dei dati. |
projectId | string | Progetto di origine dell'evento. |
data | object | Contenuto specifico dell'evento. |
Catalogo eventi
Sessioni
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
session.created | Viene aperta una nuova conversazione | Tracciamento, apertura pratica |
session.message.created | Viene aggiunto un messaggio alla sessione | Archiviazione trascrizioni |
session.tagged | Viene applicata un'etichetta alla sessione | Instradamento per categoria |
session.rated | Il visitatore lascia una valutazione | Indicatori di qualità |
session.closed | La conversazione viene chiusa | Chiusura pratica, riepilogo |
session.expired | La sessione scade per inattività | Pulizia, riconciliazione |
Handover
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
session.handover.requested | Serve un operatore umano | Creazione ticket, notifica al team |
session.handover.assigned | Un operatore prende in carico | Aggiornamento stato ticket |
session.handover.completed | L'operatore restituisce la sessione all'agente | Chiusura escalation |
session.handover.timeout | Nessun operatore disponibile entro la soglia | Escalation di secondo livello |
Clienti
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
client.created | Viene creata un'anagrafica cliente | Creazione lead nel CRM |
client.updated | Cambiano i dati anagrafici o di contatto | Allineamento CRM |
client.merged | Due anagrafiche vengono unificate | Deduplica nel gestionale |
client.consent.updated | Cambia un consenso privacy | Aggiornamento registro trattamenti |
client.deleted | L'anagrafica viene cancellata | Cancellazione a valle per GDPR |
Prenotazioni
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
booking.created | Viene creata una prenotazione | Blocco risorsa nell'ERP |
booking.confirmed | La prenotazione viene confermata | Scrittura definitiva a gestionale |
booking.rescheduled | Cambia data o orario | Aggiornamento agenda |
booking.cancelled | La prenotazione viene annullata | Liberazione risorsa |
booking.reminder.due | È il momento del promemoria | Invio SMS o email dal tuo sistema |
booking.no_show | Il cliente non si presenta | Statistiche, riaddebito |
Documenti
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
document.uploaded | Un documento viene caricato | Presa in carico |
document.classified | La classificazione automatica è completata | Deposito in DMS o protocollo |
document.extraction.completed | L'estrazione dati è terminata | Import nel gestionale |
document.rejected | Il documento è illeggibile o non conforme | Richiesta di nuovo invio |
Fatturazione
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
invoice.issued | Viene emessa una fattura | Registrazione contabile |
invoice.payment.succeeded | Il pagamento va a buon fine | Riconciliazione partitario |
invoice.payment.failed | Il pagamento fallisce | Sollecito, blocco servizio |
subscription.updated | Cambia il piano di abbonamento | Aggiornamento contratto |
Sistema
| Evento | Si attiva quando | Uso tipico |
|---|---|---|
webhook.test | Invii un evento di prova dalla Console | Verifica connettività e firma |
apikey.rotated | Una chiave API viene ruotata | Allerta al team di automazione |
webhook.delivery.failed | Una consegna esaurisce i ritentativi | Allarme operativo |
Payload di esempio
session.createdQuando scatta: viene aperta una nuova conversazione su qualsiasi canale
{
"id": "evt_01J9X4K2R7M3Q8YV5T0B6D2WQ",
"event": "session.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:20:00.412Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"sessionId": "bp_sess_3d81f0aa",
"channel": "web-widget",
"locale": "it-IT",
"startedAt": "2026-09-04T09:19:58.007Z",
"agent": {
"id": "bp_agent_1c4d",
"name": "Assistente Clienti"
},
"visitor": {
"anonymousId": "vis_9a2f4c77",
"clientId": null,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"referrer": "https://www.acme.it/assistenza"
},
"metadata": {
"campagna": "assistenza-post-vendita"
},
"traceId": "trc_6b19e0d4c8f24a7f"
}
}session.handover.requestedQuando scatta: il visitatore chiede un operatore o l agente rileva un intento critico
{
"id": "evt_01J9X5B0PZQ4T1N7C3H8M2AKE",
"event": "session.handover.requested",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:26:31.884Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"sessionId": "bp_sess_3d81f0aa",
"reason": "richiesta_esplicita",
"priority": "alta",
"queue": "assistenza-tecnica",
"requestedAt": "2026-09-04T09:26:31.512Z",
"client": {
"id": "bp_cli_82ae4f10",
"displayName": "Mario Rossi",
"email": "mario.rossi@example.it",
"phone": "+39 02 1234567"
},
"summary": "Il cliente segnala un malfunzionamento del contatore dopo la sostituzione del 2 settembre. Chiede intervento tecnico entro 48 ore.",
"lastMessages": [
{
"role": "visitor",
"text": "Il contatore continua a segnalare errore E12.",
"sentAt": "2026-09-04T09:25:40.101Z"
},
{
"role": "agent",
"text": "Verifico subito. Preferisce parlare con un tecnico?",
"sentAt": "2026-09-04T09:25:52.664Z"
},
{
"role": "visitor",
"text": "Sì, per favore.",
"sentAt": "2026-09-04T09:26:28.930Z"
}
],
"traceId": "trc_6b19e0d4c8f24a7f"
}
}session.closedQuando scatta: la conversazione viene chiusa dall operatore, dal visitatore o per timeout
{
"id": "evt_01J9X7C4WK5R2D9F0G6J1LTYB",
"event": "session.closed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:58:12.220Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"sessionId": "bp_sess_3d81f0aa",
"closedBy": "operator",
"operatorId": "bp_usr_5e73b1",
"closedAt": "2026-09-04T09:58:11.004Z",
"durationSeconds": 2293,
"messageCount": 24,
"outcome": "risolto",
"tags": ["assistenza-tecnica", "contatore", "intervento-programmato"],
"rating": {
"score": 5,
"comment": "Molto rapido, grazie."
},
"summary": "Aperto intervento tecnico PRT-2026-004512 per errore E12 sul contatore. Appuntamento fissato per il 6 settembre.",
"clientId": "bp_cli_82ae4f10",
"traceId": "trc_6b19e0d4c8f24a7f"
}
}client.createdQuando scatta: viene creata una nuova anagrafica cliente da conversazione, form o API
{
"id": "evt_01J9X8D9AN6S3E0H2K7P4MVZC",
"event": "client.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:04:45.771Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"clientId": "bp_cli_82ae4f10",
"source": "conversazione",
"sourceSessionId": "bp_sess_3d81f0aa",
"type": "azienda",
"displayName": "ACME Impianti S.r.l.",
"legalName": "ACME Impianti S.r.l.",
"vatNumber": "IT01234567890",
"taxCode": "01234567890",
"sdiCode": "SUBM70N",
"pec": "acme.impianti@pec.it",
"email": "amministrazione@acme.it",
"phone": "+39 02 1234567",
"address": {
"street": "Via Torino 42",
"city": "Milano",
"province": "MI",
"postalCode": "20123",
"country": "IT"
},
"consents": {
"marketing": false,
"profilazione": false,
"updatedAt": "2026-09-04T10:04:45.500Z"
},
"traceId": "trc_1f28a94b7c0d4e63"
}
}booking.confirmedQuando scatta: una prenotazione passa allo stato confermato
{
"id": "evt_01J9XA1F2P7T4G1J3M8R5NXQD",
"event": "booking.confirmed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:31:09.318Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"bookingId": "bp_bkg_5c907e33",
"reference": "PRE-2026-001874",
"status": "confirmed",
"confirmedAt": "2026-09-04T10:31:08.902Z",
"service": {
"id": "bp_svc_2a11",
"name": "Intervento tecnico a domicilio",
"durationMinutes": 90
},
"resource": {
"id": "bp_res_7b30",
"name": "Squadra Nord",
"type": "team"
},
"startAt": "2026-09-06T08:30:00+02:00",
"endAt": "2026-09-06T10:00:00+02:00",
"timezone": "Europe/Rome",
"client": {
"id": "bp_cli_82ae4f10",
"displayName": "ACME Impianti S.r.l.",
"email": "amministrazione@acme.it",
"phone": "+39 02 1234567"
},
"location": {
"type": "onsite",
"address": "Via Torino 42, 20123 Milano MI"
},
"customFields": {
"matricola_contatore": "CNT-99120384",
"codice_impianto": "IMP-4471"
},
"price": {
"amount": 12000,
"currency": "EUR",
"vatRate": 22
},
"sourceSessionId": "bp_sess_3d81f0aa",
"traceId": "trc_1f28a94b7c0d4e63"
}
}document.classifiedQuando scatta: la classificazione automatica di un documento caricato è completata
{
"id": "evt_01J9XB6H8Q9V5K2L4N0S7PWYE",
"event": "document.classified",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T11:02:53.640Z",
"tenantId": "bp_ten_4a91c0e5",
"projectId": "bp_proj_7f3a9c21",
"data": {
"documentId": "bp_doc_c41e08b2",
"fileName": "fattura-2026-000431.pdf",
"mimeType": "application/pdf",
"sizeBytes": 284419,
"pages": 2,
"classification": {
"category": "fattura_fornitore",
"confidence": 0.972,
"model": "bp-doc-classifier-v4"
},
"extracted": {
"numero_documento": "2026/000431",
"data_documento": "2026-08-29",
"fornitore": "Rossi Componenti S.p.A.",
"partita_iva": "IT09876543210",
"imponibile": 4520.5,
"iva": 994.51,
"totale": 5515.01,
"valuta": "EUR"
},
"downloadUrl": "https://api.blupantheon.com/identity/v1/documents/bp_doc_c41e08b2/content",
"downloadUrlExpiresAt": "2026-09-04T12:02:53.640Z",
"sourceSessionId": null,
"traceId": "trc_84cd21f0e6b74a19"
}
}URL di download temporanei
downloadUrl richiede comunque l'header Authorization con la chiave API e scade dopo un'ora.
Scarica il file appena ricevuto l'evento, non conservare l'URL.
Ritentativi e backoff
Se l'endpoint non risponde 2xx entro 10 secondi, la consegna viene ripetuta.
| Tentativo | Ritardo dal fallimento precedente | Tempo cumulato |
|---|---|---|
| 1 | Immediato | 0 |
| 2 | 1 minuto | 1 minuto |
| 3 | 5 minuti | 6 minuti |
| 4 | 30 minuti | 36 minuti |
| 5 | 2 ore | 2 ore e 36 minuti |
Esaurito il quinto tentativo, la consegna passa in dead letter e viene emesso l'evento
webhook.delivery.failed verso gli endpoint che lo hanno sottoscritto.
Codici che non vengono ritentati
Le risposte 410 Gone e 404 Not Found ripetute per oltre 24 ore causano la
disattivazione automatica dell'endpoint. Riattivalo dalla Console dopo aver risolto.
Dead letter e replay
Le consegne in dead letter restano disponibili 30 giorni. Puoi:
- filtrare per stato
Fallita, tipo di evento o intervallo temporale; - ispezionare richiesta inviata, header, corpo e risposta ricevuta;
- rilanciare una singola consegna con Riprova;
- rilanciare in blocco tutte le consegne fallite di un intervallo con Riprova selezione.
Il replay genera una nuova consegna
Il replay usa un nuovo X-BP-Delivery ma mantiene lo stesso id dell'evento. Se la tua
deduplica si basa solo su X-BP-Delivery, un replay verrà elaborato di nuovo. Per una
deduplica logica usa anche il campo id.
Idempotenza in ricezione
async function handleDelivery(deliveryId, payload) {
// livello 1: la stessa consegna (ritentativo di rete)
if (await deliveryStore.has(deliveryId)) {
return { skipped: true, reason: 'delivery-duplicata' };
}
await deliveryStore.put(deliveryId, { ttlDays: 7 });
// livello 2: lo stesso evento (replay manuale dalla Console)
if (await eventStore.has(payload.id)) {
return { skipped: true, reason: 'evento-gia-elaborato' };
}
await eventStore.put(payload.id, { ttlDays: 30 });
await enqueueForRobot(payload);
return { skipped: false };
}Test in locale
Durante lo sviluppo l'endpoint gira sulla tua macchina. Esponilo con un tunnel.
# 1. installa e autentica (una volta sola)
brew install ngrok
ngrok config add-authtoken IL_TUO_TOKEN_NGROK
# 2. avvia il tuo receiver locale sulla porta 3000
node receiver.mjs
# 3. esponi la porta 3000 su HTTPS pubblico
ngrok http 3000
# 4. copia l URL mostrato, ad esempio:
# https://a1b2-93-45-12-7.ngrok-free.app
# e registralo in Console come:
# https://a1b2-93-45-12-7.ngrok-free.app/webhooks/blupantheon
# 5. ispeziona le richieste ricevute
open http://127.0.0.1:4040Mai tunnel in produzione
I tunnel di sviluppo cambiano URL a ogni riavvio e non offrono garanzie di disponibilità. In produzione usa un endpoint stabile con certificato TLS aziendale.
Ricevitore minimo di riferimento
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.BP_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
const seenDeliveries = new Map();
app.post(
'/webhooks/blupantheon',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-BP-Signature') ?? '';
const deliveryId = req.get('X-BP-Delivery') ?? '';
const rawBody = req.body.toString('utf8');
const parts = Object.fromEntries(
signature.split(',').map((part) => part.split('=')),
);
const timestamp = Number(parts.t);
const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
if (!Number.isFinite(timestamp) || age > TOLERANCE_SECONDS) {
return res.status(400).send('timestamp fuori tolleranza');
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const provided = Buffer.from(parts.v1 ?? '', 'utf8');
const computed = Buffer.from(expected, 'utf8');
if (
provided.length !== computed.length ||
!crypto.timingSafeEqual(provided, computed)
) {
return res.status(401).send('firma non valida');
}
if (seenDeliveries.has(deliveryId)) {
return res.status(202).send();
}
seenDeliveries.set(deliveryId, Date.now());
const event = JSON.parse(rawBody);
console.log(`Ricevuto ${event.event} (${event.id})`);
queueMicrotask(() => processEvent(event));
return res.status(202).send();
},
);
function processEvent(event) {
console.log('Elaborazione asincrona di', event.id);
}
app.listen(3000, () => console.log('Receiver in ascolto sulla porta 3000'));