Webhooks
Receber webhooks
Quando algo acontece (mensagem entregue, lida, respondida, horário marcado, pagamento confirmado), a Torqyn faz um POST no endereço do seu sistema, assinado.
O endpoint
O dono cria em Configurações → Desenvolvedores → Webhooks, ou o seu sistema cria por POST /v1/webhook-endpoints (referência). Cada endpoint tem:
- Endereço: só
https, sem usuário e senha na URL, e nunca para rede privada (o endereço é resolvido e um IP interno é recusado). - Eventos: os que você escolhe do catálogo, ou todos (
*), inclusive os que entrarem depois. O que não foi escolhido não chega. - Dados do contato: por padrão, o evento leva só o id do contato na Torqyn e o id do seu sistema (
external_id). Nome, telefone e e-mail vão só se você marcar. Documento nunca vai. - Versão: a da API no dia em que o endpoint foi criado. O formato do corpo não muda por baixo do seu sistema.
Cada conta tem os seus endpoints: o de teste recebe só o que acontece na conta de teste.
O que chega
POST /seu-endereco
Content-Type: application/json
Torqyn-Signature: t=1790870400,v1=5f1c…
Torqyn-Event-Type: message.delivered
Torqyn-Delivery: 3b6d…
Torqyn-Version: 2026-10-01
{
"id": "8c1d2e3f-…",
"type": "message.delivered",
"api_version": "2026-10-01",
"created": "2026-10-01T12:00:00.000Z",
"livemode": true,
"account": "1f2e3d4c-…",
"data": { "object": { "object": "message", "status": "delivered", … } }
}
O id é do evento: é o mesmo em toda nova tentativa e no reenvio. Guarde os ids que já processou e ignore o repetido. A ordem de chegada não é garantida; use created.
Conferir a assinatura
Ao criar o endpoint, você recebe um segredo que começa com whsec_, mostrado uma vez. Cada envio leva Torqyn-Signature: t=<segundos>,v1=<hmac>, em que o hmac é o HMAC-SHA256, em hexadecimal, de <t>.<corpo cru> com o segredo.
- Confira sobre o corpo cru, os bytes que chegaram. Um
JSON.parseseguido deJSON.stringifymuda o texto e a assinatura deixa de bater. - Recuse
tcom mais de 300 segundos de diferença do seu relógio: é o que impede alguém de reenviar um corpo antigo capturado. - Compare em tempo constante, como nos exemplos.
- Na troca do segredo vêm dois
v1; basta um bater (veja abaixo).
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
// Confere o cabeçalho Torqyn-Signature sobre o corpo CRU (os bytes que chegaram, antes de
// qualquer JSON.parse). Na troca do segredo vêm dois v1: basta um bater.
export function verifyTorqynSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = String(header ?? '').split(',');
const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2));
const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!Number.isInteger(t) || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return signatures.some((s) => {
const got = Buffer.from(s, 'hex');
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
No Express, receba o corpo cru com express.raw({ type: 'application/json' }) na rota do webhook.
Python
import hashlib
import hmac
import time
def verify_torqyn_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
"""Confere o Torqyn-Signature sobre o corpo cru. Na troca do segredo vêm dois v1."""
parts = (header or "").split(",")
t = next((p[2:] for p in parts if p.startswith("t=")), "")
signatures = [p[3:] for p in parts if p.startswith("v1=")]
if not t.isdigit() or not signatures:
return False
if abs(time.time() - int(t)) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)
No Flask, o corpo cru é request.get_data(); no Django, request.body.
PHP
<?php
// Confere o Torqyn-Signature sobre o corpo cru (file_get_contents('php://input')).
// Na troca do segredo vêm dois v1: basta um bater.
function verify_torqyn_signature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
if (str_starts_with($part, 't=')) $t = substr($part, 2);
if (str_starts_with($part, 'v1=')) $signatures[] = substr($part, 3);
}
if ($t === null || !ctype_digit($t) || !$signatures) return false;
if (abs(time() - (int) $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($signatures as $s) {
if (hash_equals($expected, $s)) return true;
}
return false;
}
Os três exemplos são conferidos por teste contra a assinatura que a Torqyn gera.
Trocar o segredo sem perder evento
"Trocar o segredo" (no painel ou por POST /v1/webhook-endpoints/{id}/rotate-secret) dá um segredo novo. Por 24 horas, cada envio leva as duas assinaturas, a nova primeiro: troque no seu sistema nesse meio-tempo. Depois, só a nova.
Responder e novas tentativas
- Responda
2xxem até 10 segundos. Processe depois, em fila, se precisar de mais tempo. Qualquer outra resposta, ou nenhuma, é falha. - Falhou, a Torqyn tenta de novo depois de 1, 5, 30, 120, 360, 720 minutos, e desiste da entrega quando ela passa de 24 horas.
- Depois de 20 falhas seguidas, o endpoint é pausado e o painel mostra o motivo. O que acontecer enquanto isso fica na fila e sai quando você reativar.
- Redirecionamento para outro domínio não é seguido: responda no endereço cadastrado.
Teste, histórico e reenvio
- Evento de teste: "Enviar evento de teste" no painel, ou
POST /v1/webhook-endpoints/{id}/test, manda um eventopingna hora e devolve o que o seu servidor respondeu. - Histórico: no painel, as entregas dos últimos 30 dias, com o status HTTP, a duração e o começo do que o seu servidor respondeu.
- Reenviar: manda a mesma entrega de novo, com o mesmo id de evento, sem contar como falha.
- Pausar e desligar: pausar segura as entregas na fila; desligar para de gerar novas. O histórico fica.
Para buscar os eventos sem depender do webhook (por exemplo, depois de o seu sistema ficar fora do ar), use GET /v1/events, com os eventos dos últimos 30 dias.
De onde sai
Os webhooks saem do IP 54.94.20.141. Se o seu servidor tem firewall, libere esse IP. A assinatura continua sendo a prova: IP sozinho não autentica ninguém.