Webhooks

Receba um aviso quando algo acontecer: um lead é capturado, um visitante pede um humano, uma conversa muda de status. A OneType envia um POST assinado para o seu endpoint HTTPS.

Status: Disponível (Premium). O esquema de assinatura abaixo é final, e o código de verificação desta página foi testado com o vetor de teste oficial do Standard Webhooks.

Configure um endpoint

Em Developer › Webhooks (proprietário ou admin):

  1. Adicione uma URL HTTPS e escolha os eventos ou todos eles (*). Opcionalmente escolha Agents: o endpoint então recebe apenas os eventos desses agentes, e nenhum evento do workspace inteiro. Sem agentes escolhidos, ele recebe os eventos de todos os agentes e os do workspace inteiro.
  2. Copie o signing secret (whsec_…). Ele é mostrado uma única vez. Você pode rotacioná-lo depois.
  3. Clique em Send test para entregar um ping e veja a resposta no log de entregas.

Eventos

TipoQuando
conversation.createdUm visitante inicia uma conversa
conversation.message.createdUma mensagem é concluída (visitante, agente ou sua equipe)
conversation.handoff.requestedO visitante ou o agente pede um humano
conversation.status.changedO status mudou: open, escalated, resolved, unresolved ou failed (veja Status das conversas)
lead.capturedUm visitante deixa dados de contato
usage.threshold.reachedSeu workspace atinge 50%, 80% ou 100% dos créditos mensais (respostas em planos com chave própria)
csat.submittedUm visitante avaliou uma conversa (uma vez, definitivo). data.object é um csat_rating com conversation, agent, rating (1 a 5), handled_by (ai ou human), comment (as palavras do próprio visitante, ou null) e created_at. Ele traz o comentário, então trate-o como dado pessoal
booking.madeUm visitante marcou uma reunião pelo card de agendamento do chat (requer Cal.com ou Calendly, em breve). data.object é um booking com event_type, start_at, status, contact, conversation e matched_by (token: agendado pelo card desta conversa; email: só um e-mail verificado coincidiu, não é prova)

Mais tipos de evento serão adicionados com o tempo. Ignore os tipos que você não trata. Campos também podem ser adicionados ao envelope e aos objetos com o tempo: ignore os que você não usa.

Payload

{
  "id": "evt_01J9…",
  "type": "lead.captured",
  "created_at": "2026-09-22T10:00:00Z",
  "api_version": "v1",
  "workspace": "wsp_…",
  "agent": "agt_…",
  "data": { "object": { "id": "con_…", "object": "contact", "email": "jane@example.com", "name": "Jane" } }
}

data.object é o mesmo objeto que a REST API retorna para esse recurso. Os objetos de mensagem incluem is_ai_generated.

agent é o id do agente a que o evento pertence, na mesma forma agt_… que a REST API usa. Está presente em eventos sobre as conversas e os visitantes de um agente: conversation.created, conversation.message.created, conversation.handoff.requested, conversation.status.changed, csat.submitted, lead.captured e contact.identified. Também está em booking.made quando o agendamento veio da conversa ou de uma pessoa de um agente. Fica de fora quando nenhum agente está envolvido: eventos do workspace inteiro como usage.threshold.reached, um booking.made que não correspondeu a nenhum agente e alterações de contato que não vieram de um visitante (por exemplo, uma edição da sua equipe). Trate-o como opcional e leia o recurso em data.object. O campo é um acréscimo: os endpoints existentes passam a recebê-lo sem nenhuma mudança, então um receptor que rejeita campos desconhecidos precisa permitir o campo agent.

Verificando assinaturas

A OneType assina cada entrega com o Standard Webhooks, um esquema aberto também usado por vários grandes provedores de API. Toda requisição tem três headers:

webhook-id: evt_01J9…                        (the same on every retry of this event)
webhook-timestamp: 1790000000                 (Unix seconds)
webhook-signature: v1,K5oZfz…=               (can hold several, space-separated, during secret rotation)

A assinatura é base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), em que key é o seu segredo sem o prefixo whsec_ , decodificado em base64.

Sempre:

  • verifique o corpo bruto da requisição, antes de qualquer parse de JSON ou nova serialização
  • rejeite timestamps com mais de 5 minutos de diferença em relação a agora, o que evita replays
  • compare em tempo constante
  • aceite qualquer assinatura v1 listada, para que rotações nunca quebrem você

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyOnetypeSignature(rawBody, headers, secret, toleranceSeconds = 300)
{
    const id = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];
    const signatures = headers["webhook-signature"];
    if (!id || !timestamp || !signatures)
    {
        throw new Error("missing webhook headers");
    }
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds)
    {
        throw new Error("timestamp outside tolerance");
    }

    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();

    for (const entry of signatures.split(" "))
    {
        const [version, value] = entry.split(",");
        const candidate = Buffer.from(value ?? "", "base64");
        if (version === "v1" && candidate.length === expected.length && timingSafeEqual(candidate, expected))
        {
            return JSON.parse(rawBody);
        }
    }
    throw new Error("no matching signature");
}

// Express: keep the raw body for this route
app.post("/hooks/onetype", express.raw({ type: "application/json" }), (req, res) => {
    let event;
    try { event = verifyOnetypeSignature(req.body.toString("utf8"), req.headers, process.env.ONETYPE_WEBHOOK_SECRET); }
    catch { return res.sendStatus(400); }
    if (event.type === "lead.captured") crm.upsert(event.data.object);
    res.sendStatus(204);
});

Para rotear por agente, leia event.agent (por exemplo if (event.agent === "agt_…") …); ele não existe em eventos do workspace inteiro. Ou dê a cada agente o seu próprio endpoint com Agents (agentIds na API): uma reentrega que o endpoint não aceita mais é recusada, e um teste é assinado como um de seus agentes.

Python

import base64, hashlib, hmac, json, time

def verify_onetype_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300):
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signatures:
        raise ValueError("missing webhook headers")
    if abs(time.time() - int(timestamp)) > tolerance_seconds:
        raise ValueError("timestamp outside tolerance")

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    for entry in signatures.split(" "):
        version, _, value = entry.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return json.loads(raw_body)
    raise ValueError("no matching signature")

# Flask: request.get_data() is the raw body; request.headers is case-insensitive

PHP

<?php
function verifyOnetypeSignature(string $rawBody, array $headers, string $secret, int $toleranceSeconds = 300): array
{
    $id = $headers['webhook-id'] ?? null;
    $timestamp = $headers['webhook-timestamp'] ?? null;
    $signatures = $headers['webhook-signature'] ?? null;
    if (!$id || !$timestamp || !$signatures) {
        throw new RuntimeException('missing webhook headers');
    }
    if (abs(time() - (int) $timestamp) > $toleranceSeconds) {
        throw new RuntimeException('timestamp outside tolerance');
    }

    $key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
    $expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true));

    foreach (explode(' ', $signatures) as $entry) {
        [$version, $value] = array_pad(explode(',', $entry, 2), 2, '');
        if ($version === 'v1' && hash_equals($expected, $value)) {
            return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
        }
    }
    throw new RuntimeException('no matching signature');
}

// Usage: $raw = file_get_contents('php://input');
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);

Ferramentas no-code (Zapier, Make)

Os catch hooks básicos delas aceitam qualquer requisição e não conseguem verificar a assinatura. Trate a URL do hook como um segredo. Onde a ferramenta oferece uma etapa de código, execute ali a mesma verificação: ela precisa do corpo bruto , dos três headers webhook-* e do seu segredo. Deduplique por webhook-id de qualquer forma, porque uma nova tentativa pode entregar um evento duas vezes. Há um guia passo a passo para proprietários na central de ajuda: Conectar o Zapier ou o Make.

Verifique sua implementação com o vetor de teste do Standard Webhooks:

  • secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
  • id msg_p5jXN8AQM9LWM0D4loKWxJek
  • timestamp 1614265330
  • body {"test": 2432232314}
  • resultado esperado v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

Respostas e novas tentativas

  • Responda qualquer 2xx em até 10 segundos. Faça o trabalho lento depois de responder, por exemplo colocando-o em uma fila.
  • Em uma resposta que não seja 2xx ou em um timeout, tentamos de novo ao longo de cerca de 3 dias: após 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h e depois a cada 24 h, três vezes.
  • A entrega é pelo menos uma vez. Guarde webhook-id e ignore as repetições.
  • A ordem não é garantida. Use created_at quando a ordem importar.
  • Um endpoint que só falha durante 3 dias é desativado, e os proprietários veem um banner. Reative-o no dashboard; os novos eventos fluem a partir daí.
  • O log de entregas (30 dias) mostra cada tentativa com seu status e tempo. Redeliver envia um evento de novo.
  • Entregamos apenas a endereços HTTPS públicos e não seguimos redirecionamentos.