Webhooks
Os webhooks enviam cada conversa encerrada para o seu servidor em JSON, com o resumo da IA e os dados do respondente. Use para atualizar seu CRM, abrir tickets ou carregar os resultados no seu próprio banco de dados sem exportar nada manualmente.
O que é disparado e quando#
O Diaform envia um evento, conversation.completed. Ele é disparado quando o entrevistador de IA encerra uma conversa e o resumo fica pronto, normalmente alguns instantes depois da última mensagem.
- Para toda a organização. Cada endpoint recebe eventos de todos os projetos da sua organização. Use
agent_idno payload para diferenciar os projetos. - Conversas canceladas também disparam. Se um respondente pede para parar, a conversa termina como cancelada e você ainda recebe
conversation.completed, com"status": "cancelled". Confirastatusantes de tratar uma resposta como completa. - Conversas abandonadas não disparam. Se um respondente fecha a página sem que a conversa termine, nada é enviado.
- Prévias no painel não disparam. Para testar, conclua uma conversa pelo link real do projeto.
Planos e permissões#
Os webhooks estão disponíveis em todos os planos pagos, mas não durante o teste grátis. Cada plano tem um limite de endpoints:
| Plano | Endpoints |
|---|---|
| Starter | 3 |
| Pro | 5 |
| Business | 10 |
| Enterprise | 25 |
Só proprietários e admins podem adicionar, pausar ou excluir endpoints. Veja Membros e funções e Gerenciar sua assinatura.
Adicionar um endpoint#
Abra os webhooks
Na barra lateral, vá em Configurações → Webhooks.
Informe sua URL
Clique em Adicionar endpoint, cole sua URL na janela Novo endpoint de webhook e clique em Adicionar endpoint. A URL precisa começar com https://.
Guarde o secret
A janela Secret do webhook mostra seu secret de assinatura. Clique em Copiar, guarde-o no ambiente do seu servidor e clique em Pronto.
Cada endpoint tem o próprio secret, no formato whsec_ seguido de 43 caracteres. Você pode mostrá-lo ou copiá-lo de novo depois na linha Secret de assinatura do endpoint.
Pausar ou excluir um endpoint#
- Pausar: mude a chave na linha do endpoint de Ativo para Pausado. Os eventos que acontecem enquanto ele está pausado não são enviados depois.
- Excluir: clique no ícone de lixeira e depois em Excluir endpoint para confirmar.
Não é possível editar a URL de um endpoint. Para passar a usar uma nova URL, adicione um novo endpoint, atualize seu servidor com o secret dele e depois exclua o antigo.
Entrega#
| Comportamento | Detalhe |
|---|---|
| Método | POST, corpo em JSON |
| Distribuição | Enviado para todos os endpoints ativos ao mesmo tempo |
| Tempo limite | 5 segundos. Responda com qualquer status 2xx antes disso. |
| Novas tentativas | Nenhuma. Um tempo limite esgotado ou uma resposta diferente de 2xx não é reenviado. |
| Log de entregas | Não disponível no painel |
| Momento | Se o processamento de uma conversa concluída falhar, o Diaform tenta processar de novo a cada 30 minutos, até 4 tentativas no total. Nesse caso, o webhook pode chegar bem depois do fim da conversa. |
Responda primeiro e faça o trabalho demorado depois, por exemplo colocando o evento em uma fila.
Informe ao Diaform a URL final. Os redirecionamentos são seguidos, mas um 301 ou 302 transforma o POST em GET e o corpo se perde.
Requisição#
POST /webhooks/diaform HTTP/1.1
Content-Type: application/json
X-Webhook-Event: conversation.completed
X-Webhook-Signature: 4fa4d553f8abf7a199e5d51ee2401320af843de438e63a2ed6bc33f4f943d906| Cabeçalho | Valor |
|---|---|
Content-Type | application/json |
X-Webhook-Event | conversation.completed |
X-Webhook-Signature | HMAC-SHA256 em hexadecimal do corpo bruto, assinado com o secret do endpoint |
Não há cabeçalho de timestamp nem de ID de entrega. Use conversation_id como chave de idempotência: guarde os IDs que você já processou e ignore as repetições.
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 nível superior#
| Campo | Tipo | Descrição |
|---|---|---|
conversation_id | string | ID da conversa. Use para eliminar duplicatas. |
agent_id | string | ID do projeto ao qual a conversa pertence |
organization_id | string | ID da sua organização |
status | string | completed ou cancelled |
respondent | object | Quem é o respondente, veja abaixo |
summary | object | O resumo da IA, veja abaixo |
completed_at | string | Data e hora ISO 8601 em que a conversa terminou |
respondent#
| Campo | Tipo | Descrição |
|---|---|---|
email | string ou null | O e-mail do respondente, se for conhecido |
metadata | object | Vazio para links diretos. Em conversas incorporadas, guarda dados da incorporação, como "embed": true e embed_parent_origin. |
identities | array | Tudo o que se sabe sobre o respondente, como type, value e source |
identities[].type | string | Por exemplo email, name, identifier, user_id, account_id ou company |
identities[].source | string | owner: você informou, por um link de convite ou um identificador na incorporação. ai: extraído das mensagens do respondente. system: adicionado pelo Diaform. |
invite_id | string ou null | ID do convite, quando a conversa veio de um link de convite. Veja API de convites de respondentes. |
summary#
| Campo | Tipo | Descrição |
|---|---|---|
respondent_email | null | Campo legado, sempre null. Leia os e-mails em identities. |
respondent_identities | array | As identidades do respondente como type e value, sem source |
answers | array | Um item por pergunta |
answers[].question_id | string | ID da pergunta no seu projeto |
answers[].question_text | string | O texto da pergunta |
answers[].summary | string | Resumo curto da resposta |
answers[].sentiment | string | positive, negative, neutral ou mixed |
answers[].confidence | string | O quanto a pergunta foi respondida por completo: high, medium ou low |
answers[].quotes | array ou null | Citações de destaque do respondente. null quando não há nenhuma. |
overall_sentiment | string | positive, negative, neutral ou mixed |
urgent_issues | array, null ou ausente | Problemas críticos sinalizados pela IA |
additional_insights | string, null ou ausente | Feedback que não se encaixa em uma pergunta específica |
A transcrição e o nome do projeto não são incluídos. Para saber como o Diaform coleta identidades, veja Identificar respondentes.
Verificar a assinatura#
Confira cada requisição antes de confiar nela. O Diaform assina o corpo exato da requisição com HMAC-SHA256, usando o secret do endpoint como chave, e envia o digest em hexadecimal em X-Webhook-Signature.
- Leia o corpo bruto como bytes, antes de qualquer parsing de JSON.
- Calcule o HMAC-SHA256 desses bytes com seu secret, em hexadecimal.
- Rejeite a requisição se o cabeçalho estiver ausente ou tiver um tamanho diferente.
- Compare os dois valores com uma função de tempo constante.
Use o secret inteiro, incluindo o prefixo whsec_, como uma string UTF-8 simples. Não decodifique o secret em 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);Monte express.raw() nesta rota. Se express.json() fizer o parsing do corpo antes, os bytes mudam e todas as assinaturas falham.
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 a verificação falhar#
- Assine os bytes brutos, não um objeto JSON serializado de novo.
- Use o secret do endpoint que recebeu a requisição. Cada endpoint tem o seu.
- Verifique se sobraram espaços ou quebras de linha no secret que você guardou.