Webhook
I webhook inviano ogni conversazione conclusa al tuo server in formato JSON, con il riepilogo IA e i dati del rispondente. Usali per aggiornare il tuo CRM, aprire ticket o caricare i risultati nel tuo database senza esportare nulla a mano.
Cosa viene inviato e quando#
Diaform invia un solo evento, conversation.completed. Parte quando l’intervistatore IA chiude una conversazione e il riepilogo è pronto, di solito pochi istanti dopo l’ultimo messaggio.
- Per tutta l’organizzazione. Ogni endpoint riceve gli eventi di tutti i progetti della tua organizzazione. Usa
agent_idnel payload per distinguere i progetti. - Anche le conversazioni annullate generano l’evento. Se un rispondente chiede di fermarsi, la conversazione finisce come annullata e ricevi comunque
conversation.completed, con"status": "cancelled". Controllastatusprima di trattare una risposta come completa. - Le conversazioni abbandonate non generano eventi. Se un rispondente chiude la pagina senza che la conversazione finisca, non viene inviato nulla.
- Le anteprime della dashboard non generano eventi. Per fare una prova, completa una conversazione tramite il link reale del progetto.
Piani e permessi#
I webhook sono disponibili con tutti i piani a pagamento, non durante la prova gratuita. Ogni piano ha un limite di endpoint:
| Piano | Endpoint |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
Solo proprietari e amministratori possono aggiungere, mettere in pausa o eliminare gli endpoint. Vedi Membri e ruoli e Gestire l’abbonamento.
Aggiungi un endpoint#
Apri i webhook
Nella barra laterale, vai su Impostazioni → Webhook.
Inserisci l’URL
Clicca Aggiungi endpoint, incolla l’URL nella finestra Nuovo endpoint webhook e clicca Aggiungi endpoint. L’URL deve iniziare con https://.
Salva il segreto
La finestra Segreto del webhook mostra il tuo segreto di firma. Clicca Copia, salvalo nelle variabili d’ambiente del tuo server, poi clicca Fatto.
Ogni endpoint ha il proprio segreto, nel formato whsec_ seguito da 43 caratteri. Puoi mostrarlo o copiarlo di nuovo in seguito dalla riga Segreto di firma dell’endpoint.
Metti in pausa o elimina un endpoint#
- Mettere in pausa: sposta l’interruttore nella riga dell’endpoint da Attivo a In pausa. Gli eventi che si verificano mentre è in pausa non vengono inviati in seguito.
- Eliminare: clicca l’icona del cestino, poi Elimina endpoint per confermare.
Non puoi modificare l’URL di un endpoint. Per passare a un nuovo URL, aggiungi un nuovo endpoint, aggiorna il server con il suo segreto, poi elimina quello vecchio.
Consegna#
| Comportamento | Dettaglio |
|---|---|
| Metodo | POST, corpo JSON |
| Fan-out | Inviato a tutti gli endpoint attivi nello stesso momento |
| Timeout | 5 secondi. Rispondi entro quel tempo con uno stato 2xx qualsiasi. |
| Nuovi tentativi | Nessuno. Un timeout o una risposta non 2xx non viene reinviato. |
| Log delle consegne | Non disponibile nella dashboard |
| Tempistiche | Se l’elaborazione di una conversazione completata non riesce, Diaform la ritenta ogni 30 minuti, fino a 4 tentativi in totale. Il webhook può quindi arrivare molto dopo la fine della conversazione. |
Rispondi subito e fai il lavoro lento dopo, ad esempio mettendo l’evento in una coda.
Dai a Diaform l’URL definitivo. I reindirizzamenti vengono seguiti, ma un 301 o un 302 trasforma il POST in un GET e il corpo va perso.
Richiesta#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Header | Valore |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | HMAC-SHA256 in esadecimale del corpo grezzo, firmato con il segreto dell’endpoint |
Non c’è un header con timestamp o ID di consegna. Usa conversation_id come chiave di idempotenza: salva gli ID che hai già gestito e salta i duplicati.
Payload#
{
"conversation_id": "0f8d43a2-6df9-49c8-bf5d-fd2b6ec2d9ee",
"agent_id": "b8acb3ab-1f3d-4a7e-a846-b7f493f86b6d",
"organization_id": "4d6ecdb4-8543-4adf-ae65-1f24106d5474",
"status": "completed",
"respondent": {
"email": "alex@acme.com",
"metadata": {},
"identities": [
{ "type": "email", "value": "alex@acme.com", "source": "owner" },
{ "type": "identifier", "value": "customer-123", "source": "owner" },
{ "type": "name", "value": "Alex Morgan", "source": "ai" }
],
"invite_id": "3a4f49c7-b8f4-4c44-8f74-fd8e4277c3e4"
},
"summary": {
"respondent_email": null,
"respondent_identities": [
{ "type": "email", "value": "alex@acme.com" },
{ "type": "identifier", "value": "customer-123" },
{ "type": "name", "value": "Alex Morgan" }
],
"answers": [
{
"question_id": "V1StGXR8_Z5jdHi6B-myT",
"question_text": "How did your first week with the product go?",
"summary": "Setup was quick, but Alex struggled to invite teammates.",
"sentiment": "mixed",
"confidence": "high",
"quotes": ["I had everything running in ten minutes."]
}
],
"overall_sentiment": "mixed",
"urgent_issues": ["Teammate invites fail for Google Workspace accounts."],
"additional_insights": "Alex would pay more for an API."
},
"completed_at": "2026-05-07T12:15:41.123Z"
}Campi di primo livello#
| Campo | Tipo | Descrizione |
|---|---|---|
conversation_id | string | ID della conversazione. Usalo per eliminare i duplicati. |
agent_id | string | ID del progetto a cui appartiene la conversazione |
organization_id | string | L’ID della tua organizzazione |
status | string | completed o cancelled |
respondent | object | Chi è il rispondente, vedi sotto |
summary | object | Il riepilogo IA, vedi sotto |
completed_at | string | Data e ora ISO 8601 in cui è finita la conversazione |
respondent#
| Campo | Tipo | Descrizione |
|---|---|---|
email | string o null | L’email del rispondente, se è nota |
metadata | object | Vuoto per i link diretti. Per le conversazioni incorporate contiene i dettagli dell’embed, come "embed": true ed embed_parent_origin. |
identities | array | Tutto ciò che si sa del rispondente, come type, value e source |
identities[].type | string | Ad esempio email, name, identifier, user_id, account_id o company |
identities[].source | string | owner: l’hai fornito tu, tramite un link di invito o un identificativo dell’embed. ai: estratto dai messaggi del rispondente. system: aggiunto da Diaform. |
invite_id | string o null | ID dell’invito, se la conversazione è partita da un link di invito. Vedi API degli inviti ai rispondenti. |
summary#
| Campo | Tipo | Descrizione |
|---|---|---|
respondent_email | null | Campo legacy, sempre null. Leggi le email da identities. |
respondent_identities | array | Le identità del rispondente come type e value, senza source |
answers | array | Una voce per ogni domanda |
answers[].question_id | string | ID della domanda nel tuo progetto |
answers[].question_text | string | Il testo della domanda |
answers[].summary | string | Breve riepilogo della risposta |
answers[].sentiment | string | positive, negative, neutral o mixed |
answers[].confidence | string | Quanto è completa la risposta alla domanda: high, medium o low |
answers[].quotes | array o null | Citazioni rilevanti del rispondente. null quando non ce ne sono. |
overall_sentiment | string | positive, negative, neutral o mixed |
urgent_issues | array, null o assente | Problemi critici segnalati dall’IA |
additional_insights | string, null o assente | Feedback che non rientra in una domanda specifica |
La trascrizione e il nome del progetto non sono inclusi. Per sapere come Diaform raccoglie le identità, vedi Identificare i rispondenti.
Verifica la firma#
Controlla ogni richiesta prima di fidarti. Diaform firma il corpo esatto della richiesta con HMAC-SHA256, usando il segreto dell’endpoint come chiave, e invia il digest esadecimale in X-Webhook-Signature.
- Leggi il corpo grezzo come byte, prima di qualsiasi parsing JSON.
- Calcola l’HMAC-SHA256 di quei byte con il tuo segreto, in esadecimale.
- Rifiuta la richiesta se l’header manca o ha una lunghezza diversa.
- Confronta i due valori con una funzione a tempo costante.
Usa il segreto intero, compreso il prefisso whsec_, come semplice stringa UTF-8. Non decodificarlo in base64.
Node.js (Express)#
const crypto = require("node:crypto");
const express = require("express");
const app = express();
const secret = process.env.DIAFORM_WEBHOOK_SECRET;
function isValidSignature(rawBody, signature) {
if (!Buffer.isBuffer(rawBody) || typeof signature !== "string") {
return false;
}
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
);
const received = Buffer.from(signature);
return (
received.length === expected.length &&
crypto.timingSafeEqual(received, expected)
);
}
app.post(
"/webhooks/diaform",
express.raw({ type: "application/json" }),
(req, res) => {
if (!isValidSignature(req.body, req.get("X-Webhook-Signature"))) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200);
// Respond first, then do the slow work.
console.log(event.conversation_id, event.status);
}
);
app.listen(3000);Monta express.raw() su questa route. Se express.json() analizza prima il corpo, i byte cambiano e ogni firma fallisce.
Python (Flask)#
import hashlib
import hmac
import json
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["DIAFORM_WEBHOOK_SECRET"].encode("utf-8")
def is_valid_signature(raw_body, signature):
if not signature:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode("utf-8"), signature.encode("utf-8"))
@app.post("/webhooks/diaform")
def diaform_webhook():
raw_body = request.get_data()
if not is_valid_signature(raw_body, request.headers.get("X-Webhook-Signature")):
return "Invalid signature", 401
event = json.loads(raw_body)
print(event["conversation_id"], event["status"])
return "", 200Se la verifica non riesce#
- Firma i byte grezzi, non un oggetto JSON serializzato di nuovo.
- Usa il segreto dell’endpoint che ha ricevuto la richiesta. Ogni endpoint ha il suo.
- Controlla che nel segreto salvato non ci siano spazi o a capo di troppo.