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_iddel 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". Compruebastatusantes 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:
| Plan | Endpoints |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
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#
| Comportamiento | Detalle |
|---|---|
| Método | POST, cuerpo JSON |
| Envío múltiple | Se envía a todos los endpoints activos a la vez |
| Tiempo de espera | 5 segundos. Responde con cualquier estado 2xx antes de que pasen. |
| Reintentos | Ninguno. Un tiempo de espera agotado o una respuesta que no sea 2xx no se vuelve a enviar. |
| Registro de entregas | No disponible en el panel |
| Momento | Si 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#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Cabecera | Valor |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | HMAC-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#
{
"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#
| Campo | Tipo | Descripción |
|---|---|---|
conversation_id | string | ID de la conversación. Úsalo para eliminar duplicados. |
agent_id | string | ID del proyecto al que pertenece la conversación |
organization_id | string | El ID de tu organización |
status | string | completed o cancelled |
respondent | object | Quién es el participante, ver abajo |
summary | object | El resumen de IA, ver abajo |
completed_at | string | Momento en que terminó la conversación, en ISO 8601 |
respondent#
| Campo | Tipo | Descripción |
|---|---|---|
email | string o null | El correo del participante, si se conoce |
metadata | object | Vacío en los enlaces directos. En las conversaciones insertadas contiene datos de la inserción, como "embed": true y embed_parent_origin. |
identities | array | Todo lo que se sabe del participante, como type, value y source |
identities[].type | string | Por ejemplo email, name, identifier, user_id, account_id o company |
identities[].source | string | owner: 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_id | string o null | ID de la invitación, si la conversación vino de un enlace de invitación. Consulta API de invitaciones a participantes. |
summary#
| Campo | Tipo | Descripción |
|---|---|---|
respondent_email | null | Campo heredado, siempre null. Lee los correos de identities. |
respondent_identities | array | Las identidades del participante como type y value, sin source |
answers | array | Una entrada por pregunta |
answers[].question_id | string | ID de la pregunta en tu proyecto |
answers[].question_text | string | El texto de la pregunta |
answers[].summary | string | Resumen breve de la respuesta |
answers[].sentiment | string | positive, negative, neutral o mixed |
answers[].confidence | string | Hasta qué punto se respondió la pregunta: high, medium o low |
answers[].quotes | array o null | Citas destacadas del participante. null cuando no hay ninguna. |
overall_sentiment | string | positive, negative, neutral o mixed |
urgent_issues | array, null o ausente | Problemas críticos que marcó la IA |
additional_insights | string, null o ausente | Feedback 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.
- Lee el cuerpo sin procesar como bytes, antes de analizar el JSON.
- Calcula el HMAC-SHA256 de esos bytes con tu secreto, en hexadecimal.
- Rechaza la solicitud si falta la cabecera o si tiene otra longitud.
- 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)#
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)#
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 "", 200Si 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.