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.parse seguido de JSON.stringify muda o texto e a assinatura deixa de bater.
  • Recuse t com 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 2xx em 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 evento ping na 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.