Riferimento webhook
Schema degli eventi, catalogo completo, payload di esempio, firma HMAC, ritentativi e coda dei messaggi non recapitati.
I webhook notificano il tuo sistema in tempo reale quando accade qualcosa su Blu Pantheon, evitando il
polling degli endpoint di lettura. Ogni notifica è una richiesta POST con body JSON verso l'URL che hai
configurato, firmata con HMAC-SHA256.
Per la configurazione degli endpoint di ricezione vedi Webhook; per gli scenari di automazione vedi Automazioni.
Envelope dell'evento
Tutti gli eventi condividono la stessa struttura. I dati specifici stanno in data, i metadati fuori.
{
"id": "evt_5a9c2f18",
"type": "session.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:12:44.118Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {},
"previousAttributes": null,
"traceId": "01J8ZQ3K5V6X7Y8Z9A0B1C2D3E"
}Campi dell'envelope
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
id | string | Sì | — | Identificativo univoco dell’evento. Usalo come chiave di deduplica: lo stesso id può arrivare più volte. |
type | string | Sì | — | Tipo di evento nel formato risorsa.azione, ad esempio session.created. |
apiVersion | string | Sì | — | Versione dello schema del payload, congelata al momento della creazione della sottoscrizione. |
createdAt | string | Sì | — | Istante di generazione dell’evento in formato ISO 8601 UTC. Non coincide con l’istante di consegna. |
tenantId | string | Sì | — | Tenant a cui appartiene l’evento. Verificalo se ricevi eventi di più tenant sullo stesso endpoint. |
livemode | boolean | Sì | — | False per gli eventi generati in sandbox, true in produzione. |
data | object | Sì | — | Rappresentazione della risorsa al momento dell’evento. La struttura dipende dal tipo. |
previousAttributes | object | null | No | — | Valori precedenti dei soli campi modificati. Valorizzato negli eventi di tipo updated. |
traceId | string | Sì | — | Identificativo di correlazione della richiesta che ha generato l’evento. |
Header della richiesta
Header inviati con ogni notifica
| Parametro | Tipo | Obblig. | Default | Descrizione |
|---|---|---|---|---|
X-BP-Signature | string | Sì | — | Firma HMAC-SHA256 nel formato t=<unix>,v1=<hex>. Verificala prima di elaborare il body. |
X-BP-Event | string | Sì | — | Tipo di evento, identico al campo type del body. Utile per instradare senza deserializzare. |
X-BP-Delivery | string | Sì | — | Identificativo del singolo tentativo di consegna. Cambia a ogni ritentativo, a differenza di id. |
X-BP-Timestamp | number | Sì | — | Timestamp Unix in secondi del tentativo di consegna. Rifiuta scarti superiori a 300 secondi. |
X-BP-Attempt | number | Sì | — | Numero progressivo del tentativo, da 1 a 8. |
User-Agent | string | Sì | — | Sempre BluPantheon-Webhooks/1.0. |
Content-Type | string | Sì | — | Sempre application/json; charset=utf-8. |
POST /webhooks/blupantheon HTTP/1.1
Host: integrazioni.acme.it
Content-Type: application/json; charset=utf-8
User-Agent: BluPantheon-Webhooks/1.0
X-BP-Event: session.created
X-BP-Delivery: dlv_3f7a1c60
X-BP-Timestamp: 1788515564
X-BP-Attempt: 1
X-BP-Signature: t=1788515564,v1=9c4f2a...Versionamento dei payload
Ogni sottoscrizione è ancorata a una versione dello schema, indicata in apiVersion. La versione viene
fissata quando crei la sottoscrizione e non cambia automaticamente.
| Comportamento | Effetto sulla versione |
|---|---|
Aggiunta di un campo in data | Nessuno, viene distribuito su tutte le versioni |
| Aggiunta di un nuovo tipo di evento | Nessuno, va sottoscritto esplicitamente |
| Rinomina o rimozione di un campo | Nuova versione dello schema |
| Cambio di tipo di un campo | Nuova versione dello schema |
Ignora i campi sconosciuti
Il tuo consumatore deve tollerare campi aggiuntivi non previsti: possono comparire in qualsiasi momento senza cambio di versione. Non usare deserializzatori configurati per fallire su proprietà sconosciute.
L'aggiornamento a una nuova versione si effettua dalla Console.
Catalogo degli eventi
| Evento | Dominio | Attivazione |
|---|---|---|
session.created | Sessioni | Una nuova conversazione viene avviata su qualunque canale |
session.message.created | Sessioni | Viene aggiunto un messaggio alla conversazione |
session.closed | Sessioni | La conversazione viene chiusa manualmente o per inattività |
session.abandoned | Sessioni | L'utente interrompe la conversazione senza chiuderla |
session.rated | Sessioni | Il cliente lascia una valutazione al termine della conversazione |
session.tagged | Sessioni | Viene applicata o rimossa un'etichetta |
handover.requested | Handover | Viene richiesto il passaggio a un operatore umano |
handover.assigned | Handover | Un operatore prende in carico la conversazione |
handover.completed | Handover | L'operatore restituisce la conversazione o la chiude |
handover.expired | Handover | Nessun operatore ha preso in carico entro il tempo massimo |
client.created | Clienti | Viene creata una nuova anagrafica cliente |
client.updated | Clienti | Un campo dell'anagrafica viene modificato |
client.merged | Clienti | Due anagrafiche duplicate vengono unificate |
client.deleted | Clienti | L'anagrafica viene eliminata definitivamente |
booking.created | Prenotazioni | Viene creata una prenotazione in stato pending |
booking.confirmed | Prenotazioni | La prenotazione viene confermata |
booking.rescheduled | Prenotazioni | Data o ora della prenotazione vengono modificate |
booking.cancelled | Prenotazioni | La prenotazione viene annullata |
booking.reminder | Prenotazioni | Scatta il promemoria configurato prima dell'appuntamento |
document.uploaded | Documenti | Un documento viene caricato in una knowledge base |
document.indexed | Documenti | L'indicizzazione è completata e il documento è utilizzabile |
document.failed | Documenti | L'elaborazione del documento non è riuscita |
document.deleted | Documenti | Un documento viene rimosso dalla knowledge base |
invoice.issued | Fatturazione | Viene emessa una nuova fattura |
invoice.paid | Fatturazione | Il pagamento della fattura viene registrato |
invoice.payment_failed | Fatturazione | Il tentativo di addebito non è andato a buon fine |
subscription.updated | Fatturazione | Cambia piano, ciclo o stato dell'abbonamento |
usage.threshold_reached | Fatturazione | Il consumo supera una soglia configurata del piano |
user.invited | Identità | Un utente viene invitato nel tenant |
user.activated | Identità | L'utente completa l'attivazione dell'account |
user.deactivated | Identità | L'utente viene disattivato |
api_key.revoked | Sistema | Una chiave API viene revocata |
project.published | Sistema | Un progetto viene pubblicato o ripubblicato |
Payload di esempio
session.createdQuando scatta: quando una nuova conversazione viene avviata su qualunque canale
{
"id": "evt_5a9c2f18",
"type": "session.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:12:44.118Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "sess_9f2c8d41",
"projectId": "bp_proj_7f3a9c21",
"clientId": "cli_5e1a7b30",
"channel": "web",
"status": "open",
"language": "it",
"metadata": {
"sourceUrl": "https://www.acme.it/supporto",
"userAgent": "Mozilla/5.0",
"utmCampaign": "settembre-2026"
},
"startedAt": "2026-09-04T09:12:44.006Z"
},
"previousAttributes": null,
"traceId": "01J8ZQ3K5V6X7Y8Z9A0B1C2D3E"
}session.message.createdQuando scatta: quando viene aggiunto un messaggio alla conversazione
{
"id": "evt_7b1e4c93",
"type": "session.message.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:12:46.980Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "msg_4f9a2d18",
"sessionId": "sess_9f2c8d41",
"projectId": "bp_proj_7f3a9c21",
"role": "assistant",
"content": "Certamente. Mi conferma la ragione sociale e la partita IVA?",
"citations": [
{ "documentId": "doc_2e8b4c07", "title": "Listino 2026", "score": 0.87 }
],
"tokensUsed": 184,
"createdAt": "2026-09-04T09:12:46.914Z"
},
"previousAttributes": null,
"traceId": "01J8ZQ3K5V6X7Y8Z9A0B1C2D3F"
}session.closedQuando scatta: quando la conversazione viene chiusa manualmente o per inattività
{
"id": "evt_2d8f0a71",
"type": "session.closed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:02:11.640Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "sess_9f2c8d41",
"projectId": "bp_proj_7f3a9c21",
"clientId": "cli_5e1a7b30",
"status": "closed",
"reason": "resolved",
"messageCount": 14,
"metrics": {
"durationSeconds": 1698,
"averageResponseMs": 1240,
"tokensUsed": 8412
},
"closedAt": "2026-09-04T10:02:11.556Z"
},
"previousAttributes": { "status": "open" },
"traceId": "01J8ZQ4M7X2P9Q1R3S5T7U9V1W"
}handover.requestedQuando scatta: quando viene richiesto il passaggio a un operatore umano
{
"id": "evt_9c3a7e02",
"type": "handover.requested",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:44:18.302Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "hnd_7a3c5e01",
"sessionId": "sess_9f2c8d41",
"projectId": "bp_proj_7f3a9c21",
"clientId": "cli_5e1a7b30",
"queue": "vendite",
"priority": "high",
"reason": "Richiesta di sconto fuori listino",
"status": "pending",
"position": 2,
"context": { "dealSize": "50 licenze" },
"requestedAt": "2026-09-04T09:44:18.220Z"
},
"previousAttributes": null,
"traceId": "01J8ZQ5N8Y3R0S2T4U6V8W0X2Y"
}handover.assignedQuando scatta: quando un operatore prende in carico la conversazione
{
"id": "evt_1e6b9d40",
"type": "handover.assigned",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T09:46:03.117Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "hnd_7a3c5e01",
"sessionId": "sess_9f2c8d41",
"queue": "vendite",
"status": "assigned",
"operator": {
"id": "usr_4a7b1e90",
"email": "mario.rossi@acme.it",
"displayName": "Mario Rossi"
},
"waitSeconds": 105,
"assignedAt": "2026-09-04T09:46:03.041Z"
},
"previousAttributes": { "status": "pending", "position": 2 },
"traceId": "01J8ZQ6P9Z4S1T3U5V7W9X1Y3Z"
}client.createdQuando scatta: quando viene creata una nuova anagrafica cliente
{
"id": "evt_6f2c1a85",
"type": "client.created",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:32:15.560Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "cli_5e1a7b30",
"email": "mario.rossi@acme.it",
"firstName": "Mario",
"lastName": "Rossi",
"phoneNumber": "+390212345678",
"company": "ACME S.p.A.",
"externalId": "CRM-88213",
"source": "web",
"tags": ["lead-inbound"],
"consent": { "marketing": false, "profiling": false },
"createdAt": "2026-09-04T10:32:15.482Z"
},
"previousAttributes": null,
"traceId": "01J8ZQ7Q0A5T2U4V6W8X0Y2Z4A"
}client.updatedQuando scatta: quando un campo dell'anagrafica cliente viene modificato
{
"id": "evt_8a4d3b70",
"type": "client.updated",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T11:11:02.712Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "cli_5e1a7b30",
"email": "mario.rossi@acme.it",
"phoneNumber": "+390687654321",
"tags": ["cliente-storico", "rinnovo-2027"],
"updatedAt": "2026-09-04T11:11:02.640Z"
},
"previousAttributes": {
"phoneNumber": "+390212345678",
"tags": ["cliente-storico"]
},
"traceId": "01J8ZQ8R1B6U3V5W7X9Y1Z3A5B"
}booking.confirmedQuando scatta: quando una prenotazione passa in stato confermato
{
"id": "evt_3c9e5f21",
"type": "booking.confirmed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:35:02.844Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "bkg_2a6f9c34",
"serviceId": "svc_1d9a4f70",
"serviceName": "Consulenza commerciale",
"resourceId": "res_5c2e8a13",
"clientId": "cli_5e1a7b30",
"sessionId": "sess_9f2c8d41",
"status": "confirmed",
"start": "2026-09-08T07:00:00Z",
"end": "2026-09-08T07:30:00Z",
"timezone": "Europe/Rome",
"confirmationCode": "ACME-4821",
"confirmedAt": "2026-09-04T10:35:02.771Z"
},
"previousAttributes": { "status": "pending" },
"traceId": "01J8ZQ9S2C7V4W6X8Y0Z2A4B6C"
}booking.cancelledQuando scatta: quando una prenotazione viene annullata e lo slot torna disponibile
{
"id": "evt_4d0f6a32",
"type": "booking.cancelled",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-05T08:12:40.096Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "bkg_2a6f9c34",
"serviceId": "svc_1d9a4f70",
"clientId": "cli_5e1a7b30",
"status": "cancelled",
"reason": "client_request",
"note": "Il cliente ha chiesto di rimandare.",
"cancelledBy": "usr_4a7b1e90",
"start": "2026-09-08T07:00:00Z",
"cancelledAt": "2026-09-05T08:12:40.019Z"
},
"previousAttributes": { "status": "confirmed" },
"traceId": "01J8ZQAT3D8W5X7Y9Z1A3B5C7D"
}document.indexedQuando scatta: quando l'indicizzazione di un documento è completata
{
"id": "evt_5e1a7b43",
"type": "document.indexed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:34:58.201Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "doc_2e8b4c07",
"dataStoreId": "ds_3c8f1a60",
"title": "Listino 2026",
"fileName": "listino-2026.pdf",
"mimeType": "application/pdf",
"sizeBytes": 1843205,
"status": "ready",
"pageCount": 24,
"chunkCount": 187,
"language": "it",
"processingSeconds": 162,
"indexedAt": "2026-09-04T10:34:58.140Z"
},
"previousAttributes": { "status": "processing" },
"traceId": "01J8ZQBU4E9X6Y8Z0A2B4C6D8E"
}document.failedQuando scatta: quando l'elaborazione di un documento non riesce
{
"id": "evt_6f2b8c54",
"type": "document.failed",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T10:36:12.508Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "doc_9a1d3e78",
"dataStoreId": "ds_3c8f1a60",
"fileName": "catalogo-scansionato.pdf",
"status": "failed",
"error": {
"code": "UNREADABLE_CONTENT",
"message": "Il PDF non contiene testo estraibile. Attiva il riconoscimento ottico dei caratteri."
},
"failedAt": "2026-09-04T10:36:12.441Z"
},
"previousAttributes": { "status": "processing" },
"traceId": "01J8ZQCV5F0Y7Z9A1B3C5D7E9F"
}invoice.paidQuando scatta: quando il pagamento di una fattura viene registrato
{
"id": "evt_7a3c9d65",
"type": "invoice.paid",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-01T06:04:33.201Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"id": "inv_0f3d8b52",
"number": "2026/000418",
"status": "paid",
"subscriptionId": "sub_4b7e1a09",
"total": { "amount": 60878, "currency": "EUR" },
"taxAmount": { "amount": 10978, "currency": "EUR" },
"paymentMethod": { "type": "sepa_debit", "last4": "4821" },
"periodStart": "2026-09-01T00:00:00Z",
"periodEnd": "2026-10-01T00:00:00Z",
"pdfUrl": "https://files.blupantheon.com/invoices/inv_0f3d8b52.pdf?sig=...",
"paidAt": "2026-09-01T06:04:33.117Z"
},
"previousAttributes": { "status": "open" },
"traceId": "01J8ZQDW6G1Z8A0B2C4D6E8F0G"
}usage.threshold_reachedQuando scatta: quando il consumo supera una soglia configurata del piano
{
"id": "evt_8b4d0e76",
"type": "usage.threshold_reached",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-04T13:02:00.114Z",
"tenantId": "tnt_2b8e4f11",
"livemode": true,
"data": {
"metric": "sessions",
"threshold": 0.8,
"used": 16041,
"included": 20000,
"unit": "sessione",
"periodStart": "2026-09-01T00:00:00Z",
"periodEnd": "2026-10-01T00:00:00Z",
"plan": "business",
"overagePolicy": "billed"
},
"previousAttributes": null,
"traceId": "01J8ZQEX7H2A9B1C3D5E7F9G1H"
}Firma e verifica
L'header X-BP-Signature ha due componenti separate da virgola:
X-BP-Signature: t=1788515564,v1=9c4f2a1e8b7d0345f6a9c2e1b8d7043a5f6c9e2b1d8a7043f5c6e9b2d1a8074f| Componente | Significato |
|---|---|
t | Timestamp Unix in secondi del tentativo di consegna |
v1 | HMAC-SHA256 esadecimale minuscolo, versione 1 dello schema di firma |
La stringa firmata è la concatenazione del timestamp, un punto e il corpo grezzo della richiesta:
${timestamp}.${rawBody}Usa il body grezzo
La verifica deve avvenire sui byte esatti ricevuti, prima di qualsiasi parsing JSON. Se il framework deserializza automaticamente il body e tu lo riserializzi per calcolare l'HMAC, la firma non corrisponderà mai: l'ordine delle chiavi e la spaziatura cambiano.
Passaggi di verifica:
Estrai le componenti
Separa t e v1 dall'header. Se una delle due manca, rifiuta la richiesta con 400.
Verifica la finestra temporale
Rifiuta le richieste con t che differisce di oltre 300 secondi dall'ora corrente: è la difesa contro
gli attacchi di replay.
Calcola e confronta
Calcola HMAC-SHA256(segreto, "${t}.${rawBody}") e confrontalo con v1 usando un confronto a tempo
costante. Non usare l'operatore di uguaglianza tra stringhe.
Rispondi rapidamente
Restituisci 2xx entro 5 secondi e processa l'evento in modo asincrono. Un'elaborazione lenta viene
interpretata come fallimento e genera ritentativi.
L'implementazione completa nei vari linguaggi, con la rotazione dei segreti, è documentata in Firma HMAC.
Ritentativi
Una consegna è considerata riuscita solo con status 2xx. Qualsiasi altro status, timeout o errore di
connessione avvia la sequenza di ritentativi con backoff esponenziale.
| Tentativo | Attesa dal precedente | Tempo trascorso |
|---|---|---|
| 1 | Immediato | 0 |
| 2 | 30 secondi | 30 secondi |
| 3 | 2 minuti | circa 2 minuti |
| 4 | 10 minuti | circa 12 minuti |
| 5 | 1 ora | circa 1 ora |
| 6 | 6 ore | circa 7 ore |
| 7 | 24 ore | circa 31 ore |
| 8 | 48 ore | circa 79 ore |
| Risposta del tuo endpoint | Comportamento |
|---|---|
2xx | Consegna completata |
3xx | Fallimento: i redirect non vengono seguiti |
410 Gone | Consegna abbandonata immediatamente, nessun ritentativo |
4xx (altri) | Ritentativo secondo la tabella |
5xx | Ritentativo secondo la tabella |
| Timeout oltre 5 secondi | Ritentativo secondo la tabella |
Consegna almeno una volta
Lo stesso evento può essere consegnato più volte, anche se il tuo endpoint ha risposto correttamente
(ad esempio se la risposta è andata persa). Il tuo consumatore deve essere idempotente: usa il
campo id dell'envelope come chiave di deduplica e ignora gli eventi già elaborati.
L'header X-BP-Delivery cambia a ogni tentativo e non va usato per la deduplica.
Coda dei messaggi non recapitati
Dopo l'ottavo tentativo fallito l'evento viene spostato nella coda dei messaggi non recapitati e vi resta per 30 giorni.
Comportamento della sospensione automatica:
- Se un endpoint fallisce tutte le consegne per 24 ore consecutive viene sospeso automaticamente.
- Alla sospensione vengono notificati via email gli amministratori del tenant.
- Gli eventi generati durante la sospensione vengono comunque accodati per 30 giorni.
- La riattivazione è manuale e avviene dalla Console dopo aver risolto il problema.
Replay
Puoi rinviare eventi già consegnati o falliti, dalla Console o via API. È l'operazione da usare dopo un fermo del tuo sistema o dopo la correzione di un bug nel consumatore.
/console/v1/webhooks/{endpointId}/replayBearer JWTRinvia gli eventi selezionati all'endpoint indicato. Accetta Idempotency-Key.
{
"eventIds": ["evt_5a9c2f18", "evt_7b1e4c93"],
"from": "2026-09-04T00:00:00Z",
"to": "2026-09-04T23:59:59Z",
"eventTypes": ["session.created", "session.closed"],
"onlyFailed": true
}{
"replayId": "rpl_1b8c4e07",
"endpointId": "whe_6d2f9a31",
"eventCount": 128,
"status": "queued",
"createdAt": "2026-09-05T09:00:12.447Z"
}Vincoli del replay:
- Massimo 1.000 eventi per singola operazione.
- Finestra massima di 30 giorni.
- Gli eventi rinviati mantengono
idecreatedAtoriginali, ma ricevono un nuovoX-BP-Delivery. - Il campo
X-BP-Attemptriparte da 1.
Verifica l'idempotenza prima di un replay massivo
Un replay di centinaia di eventi su un consumatore non idempotente produce duplicati in cascata nel tuo sistema. Prima di lanciarlo, verifica su un singolo evento che l'elaborazione ripetuta non generi effetti collaterali.