Integraciones

Webhooks

Los webhooks envían cada conversación terminada a tu servidor como JSON, con el resumen de IA y los datos del participante. Úsalos para actualizar tu CRM, abrir tickets o cargar los resultados en tu propia base de datos sin exportar nada a mano.

Qué se envía y cuándo#

Diaform envía un solo evento, conversation.completed. Se envía cuando el entrevistador de IA termina una conversación y el resumen está listo, normalmente unos instantes después del último mensaje.

  • Para toda la organización. Cada endpoint recibe los eventos de todos los proyectos de tu organización. Usa agent_id del payload para distinguir los proyectos.
  • Las conversaciones canceladas también se envían. Si un participante pide parar, la conversación termina como cancelada y aun así recibes conversation.completed, con "status": "cancelled". Comprueba status antes de tratar una respuesta como completa.
  • Las conversaciones abandonadas no se envían. Si un participante cierra la página sin que la conversación termine, no se envía nada.
  • Las vistas previas del panel no se envían. Para probar, termina una conversación con el enlace real del proyecto.

Planes y permisos#

Los webhooks están disponibles en todos los planes de pago, no durante la prueba gratis. Cada plan tiene un límite de endpoints:

PlanEndpoints
Starter3
Pro5
Business10
Enterprise25

Solo los propietarios y los administradores pueden añadir, pausar o eliminar endpoints. Consulta Miembros y roles y Gestionar tu suscripción.

Añadir un endpoint#

Abre los webhooks

En la barra lateral, ve a Configuración → Webhooks.

Escribe tu URL

Haz clic en Añadir endpoint, pega tu URL en el cuadro de diálogo Nuevo endpoint de webhook y haz clic en Añadir endpoint. La URL debe empezar por https://.

Guarda el secreto

El cuadro de diálogo Secreto del webhook muestra tu secreto de firma. Haz clic en Copiar, guárdalo en las variables de entorno de tu servidor y haz clic en Listo.

Cada endpoint tiene su propio secreto, con el formato whsec_ seguido de 43 caracteres. Puedes volver a mostrarlo o copiarlo más adelante desde la línea Secreto de firma de la fila del endpoint.

Pausar o eliminar un endpoint#

  • Pausar: cambia el interruptor de la fila del endpoint de Activo a Pausado. Los eventos que ocurren mientras está pausado no se envían después.
  • Eliminar: haz clic en el icono de la papelera y después en Eliminar endpoint para confirmar.

No puedes editar la URL de un endpoint. Para pasar a una URL nueva, añade un endpoint nuevo, actualiza tu servidor con su secreto y elimina el anterior.

Entrega#

ComportamientoDetalle
MétodoPOST, cuerpo JSON
Envío múltipleSe envía a todos los endpoints activos a la vez
Tiempo de espera5 segundos. Responde con cualquier estado 2xx antes de que pasen.
ReintentosNinguno. Un tiempo de espera agotado o una respuesta que no sea 2xx no se vuelve a enviar.
Registro de entregasNo disponible en el panel
MomentoSi falla el procesamiento de una conversación completada, Diaform reintenta el procesamiento cada 30 minutos, hasta 4 intentos en total. El webhook puede llegar entonces mucho después de que terminara la conversación.

Responde primero y haz el trabajo lento después, por ejemplo poniendo el evento en una cola.

Dale a Diaform la URL final. Se siguen las redirecciones, pero un 301 o un 302 convierte el POST en un GET y se pierde el cuerpo.

Solicitud#

HTTP
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906
CabeceraValor
Content-Typeapplication/json
X-Webhook-Eventconversation.completed
X-Webhook-SignatureHMAC-SHA256 en hexadecimal del cuerpo sin procesar, firmado con el secreto del endpoint

No hay cabecera de marca de tiempo ni de ID de entrega. Usa conversation_id como clave de idempotencia: guarda los ID que ya has procesado y omite los repetidos.

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"
}

Campos de nivel superior#

CampoTipoDescripción
conversation_idstringID de la conversación. Úsalo para eliminar duplicados.
agent_idstringID del proyecto al que pertenece la conversación
organization_idstringEl ID de tu organización
statusstringcompleted o cancelled
respondentobjectQuién es el participante, ver abajo
summaryobjectEl resumen de IA, ver abajo
completed_atstringMomento en que terminó la conversación, en ISO 8601

respondent#

CampoTipoDescripción
emailstring o nullEl correo del participante, si se conoce
metadataobjectVacío en los enlaces directos. En las conversaciones insertadas contiene datos de la inserción, como "embed": true y embed_parent_origin.
identitiesarrayTodo lo que se sabe del participante, como type, value y source
identities[].typestringPor ejemplo email, name, identifier, user_id, account_id o company
identities[].sourcestringowner: lo aportaste tú, con un enlace de invitación o un identificador en el widget. ai: extraído de los mensajes del participante. system: añadido por Diaform.
invite_idstring o nullID de la invitación, si la conversación vino de un enlace de invitación. Consulta API de invitaciones a participantes.

summary#

CampoTipoDescripción
respondent_emailnullCampo heredado, siempre null. Lee los correos de identities.
respondent_identitiesarrayLas identidades del participante como type y value, sin source
answersarrayUna entrada por pregunta
answers[].question_idstringID de la pregunta en tu proyecto
answers[].question_textstringEl texto de la pregunta
answers[].summarystringResumen breve de la respuesta
answers[].sentimentstringpositive, negative, neutral o mixed
answers[].confidencestringHasta qué punto se respondió la pregunta: high, medium o low
answers[].quotesarray o nullCitas destacadas del participante. null cuando no hay ninguna.
overall_sentimentstringpositive, negative, neutral o mixed
urgent_issuesarray, null o ausenteProblemas críticos que marcó la IA
additional_insightsstring, null o ausenteFeedback que no encaja en ninguna pregunta concreta

No se incluyen la transcripción ni el nombre del proyecto. Para saber cómo recoge Diaform las identidades, consulta Identificar a los participantes.

Verificar la firma#

Comprueba cada solicitud antes de fiarte de ella. Diaform firma el cuerpo exacto de la solicitud con HMAC-SHA256, usando el secreto del endpoint como clave, y envía el resultado en hexadecimal en X-Webhook-Signature.

  1. Lee el cuerpo sin procesar como bytes, antes de analizar el JSON.
  2. Calcula el HMAC-SHA256 de esos bytes con tu secreto, en hexadecimal.
  3. Rechaza la solicitud si falta la cabecera o si tiene otra longitud.
  4. Compara los dos valores con una función de tiempo constante.

Usa el secreto entero, incluido el prefijo whsec_, como una cadena UTF-8 normal. No lo decodifiques de 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() en esta ruta. Si express.json() analiza antes el cuerpo, los bytes cambian y todas las firmas fallan.

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

Si la verificación falla#

  • Firma los bytes sin procesar, no un objeto JSON vuelto a serializar.
  • Usa el secreto del endpoint que recibió la solicitud. Cada endpoint tiene el suyo.
  • Revisa que el secreto que guardaste no tenga espacios ni saltos de línea de más.