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):
- 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. - Copie o signing secret (
whsec_…). Ele é mostrado uma única vez. Você pode rotacioná-lo depois. - Clique em Send test para entregar um
pinge veja a resposta no log de entregas.
Eventos
| Tipo | Quando |
|---|---|
conversation.created | Um visitante inicia uma conversa |
conversation.message.created | Uma mensagem é concluída (visitante, agente ou sua equipe) |
conversation.handoff.requested | O visitante ou o agente pede um humano |
conversation.status.changed | O status mudou: open, escalated, resolved, unresolved ou failed (veja Status das conversas) |
lead.captured | Um visitante deixa dados de contato |
usage.threshold.reached | Seu workspace atinge 50%, 80% ou 100% dos créditos mensais (respostas em planos com chave própria) |
csat.submitted | Um 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.made | Um 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
v1listada, 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-insensitivePHP
<?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-ide ignore as repetições. - A ordem não é garantida. Use
created_atquando 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.