Integrazioni

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_id nel 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". Controlla status prima 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:

PianoEndpoint
Starter3
Pro5
Business10
Enterprise25

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#

ComportamentoDettaglio
MetodoPOST, corpo JSON
Fan-outInviato a tutti gli endpoint attivi nello stesso momento
Timeout5 secondi. Rispondi entro quel tempo con uno stato 2xx qualsiasi.
Nuovi tentativiNessuno. Un timeout o una risposta non 2xx non viene reinviato.
Log delle consegneNon disponibile nella dashboard
TempisticheSe 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#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
HeaderValore
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHMAC-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#

JSON
{
  "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#

CampoTipoDescrizione
conversation_idstringID della conversazione. Usalo per eliminare i duplicati.
agent_idstringID del progetto a cui appartiene la conversazione
organization_idstringL’ID della tua organizzazione
statusstringcompleted o cancelled
respondentobjectChi è il rispondente, vedi sotto
summaryobjectIl riepilogo IA, vedi sotto
completed_atstringData e ora ISO 8601 in cui è finita la conversazione

respondent#

CampoTipoDescrizione
emailstring o nullL’email del rispondente, se è nota
metadataobjectVuoto per i link diretti. Per le conversazioni incorporate contiene i dettagli dell’embed, come "embed": true ed embed_parent_origin.
identitiesarrayTutto ciò che si sa del rispondente, come type, value e source
identities[].typestringAd esempio email, name, identifier, user_id, account_id o company
identities[].sourcestringowner: 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_idstring o nullID dell’invito, se la conversazione è partita da un link di invito. Vedi API degli inviti ai rispondenti.

summary#

CampoTipoDescrizione
respondent_emailnullCampo legacy, sempre null. Leggi le email da identities.
respondent_identitiesarrayLe identità del rispondente come type e value, senza source
answersarrayUna voce per ogni domanda
answers[].question_idstringID della domanda nel tuo progetto
answers[].question_textstringIl testo della domanda
answers[].summarystringBreve riepilogo della risposta
answers[].sentimentstringpositive, negative, neutral o mixed
answers[].confidencestringQuanto è completa la risposta alla domanda: high, medium o low
answers[].quotesarray o nullCitazioni rilevanti del rispondente. null quando non ce ne sono.
overall_sentimentstringpositive, negative, neutral o mixed
urgent_issuesarray, null o assenteProblemi critici segnalati dall’IA
additional_insightsstring, null o assenteFeedback 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.

  1. Leggi il corpo grezzo come byte, prima di qualsiasi parsing JSON.
  2. Calcola l’HMAC-SHA256 di quei byte con il tuo segreto, in esadecimale.
  3. Rifiuta la richiesta se l’header manca o ha una lunghezza diversa.
  4. 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)#

JavaScript
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)#

Python
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 "", 200

Se 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.