Integrações

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_id no 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". Confira status antes 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:

PlanoEndpoints
Starter3
Pro5
Business10
Enterprise25

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#

ComportamentoDetalhe
MétodoPOST, corpo em JSON
DistribuiçãoEnviado para todos os endpoints ativos ao mesmo tempo
Tempo limite5 segundos. Responda com qualquer status 2xx antes disso.
Novas tentativasNenhuma. Um tempo limite esgotado ou uma resposta diferente de 2xx não é reenviado.
Log de entregasNão disponível no painel
MomentoSe 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#

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

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 nível superior#

CampoTipoDescrição
conversation_idstringID da conversa. Use para eliminar duplicatas.
agent_idstringID do projeto ao qual a conversa pertence
organization_idstringID da sua organização
statusstringcompleted ou cancelled
respondentobjectQuem é o respondente, veja abaixo
summaryobjectO resumo da IA, veja abaixo
completed_atstringData e hora ISO 8601 em que a conversa terminou

respondent#

CampoTipoDescrição
emailstring ou nullO e-mail do respondente, se for conhecido
metadataobjectVazio para links diretos. Em conversas incorporadas, guarda dados da incorporação, como "embed": true e embed_parent_origin.
identitiesarrayTudo o que se sabe sobre o respondente, como type, value e source
identities[].typestringPor exemplo email, name, identifier, user_id, account_id ou company
identities[].sourcestringowner: 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_idstring ou nullID do convite, quando a conversa veio de um link de convite. Veja API de convites de respondentes.

summary#

CampoTipoDescrição
respondent_emailnullCampo legado, sempre null. Leia os e-mails em identities.
respondent_identitiesarrayAs identidades do respondente como type e value, sem source
answersarrayUm item por pergunta
answers[].question_idstringID da pergunta no seu projeto
answers[].question_textstringO texto da pergunta
answers[].summarystringResumo curto da resposta
answers[].sentimentstringpositive, negative, neutral ou mixed
answers[].confidencestringO quanto a pergunta foi respondida por completo: high, medium ou low
answers[].quotesarray ou nullCitações de destaque do respondente. null quando não há nenhuma.
overall_sentimentstringpositive, negative, neutral ou mixed
urgent_issuesarray, null ou ausenteProblemas críticos sinalizados pela IA
additional_insightsstring, null ou ausenteFeedback 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.

  1. Leia o corpo bruto como bytes, antes de qualquer parsing de JSON.
  2. Calcule o HMAC-SHA256 desses bytes com seu secret, em hexadecimal.
  3. Rejeite a requisição se o cabeçalho estiver ausente ou tiver um tamanho diferente.
  4. 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)#

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);

Monte express.raw() nesta rota. Se express.json() fizer o parsing do corpo antes, os bytes mudam e todas as assinaturas falham.

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