Configuração e segurança

Como proteger seu endpoint de webhook: validação da assinatura HMAC-SHA256 (X-IORQ-Signature), defesa contra replay, headers custom como camada adicional, TLS e o que responder a cada entrega.

1. Assinatura HMAC das entregas

Toda entrega de webhook é assinada com HMAC-SHA256 usando o signing_secret do webhook. Dois headers acompanham cada POST:

HeaderConteúdo
X-IORQ-Signaturet=<unix_ts>,v1=<hex> — o digest cobre "{timestamp}.{corpo_bruto}"
X-IORQ-TimestampO mesmo unix_ts, para conveniência

O signing_secret (prefixo whsec_) é retornado uma única vez, na resposta do POST /webhook — guarde-o em cofre imediatamente. GET, PATCH e listagem não o retornam.

🚧

Webhooks criados antes da assinatura

Webhooks registrados antes da disponibilização da assinatura não têm signing_secret e seguem sendo entregues sem os headers acima (nada muda para eles). Para passar a receber entregas assinadas, registre um novo webhook.

Como validar do seu lado

  1. Extraia t e v1 do header X-IORQ-Signature
  2. Concatene: signed_payload = t + "." + corpo_bruto (use os bytes exatos do body recebido, antes de qualquer parse)
  3. Calcule HMAC-SHA256(signing_secret, signed_payload)
  4. Compare com v1 em tempo constante (hmac.compare_digest em Python ou equivalente)
  5. Rejeite se divergir, ou se t estiver a mais de 5 minutos do relógio do seu servidor

Implementações

# Python
import hmac, hashlib, time

def verify(payload_bytes, signature_header, secret, tolerance=300):
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts, sig = parts["t"], parts["v1"]
    if abs(time.time() - int(ts)) > tolerance:
        return False
    signed = f"{ts}.{payload_bytes.decode()}"
    expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
// Node.js
const crypto = require('crypto');

function verify(payloadBytes, signatureHeader, secret, tolerance = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('='))
  );
  const { t: ts, v1: sig } = parts;
  if (Math.abs(Date.now() / 1000 - parseInt(ts)) > tolerance) return false;
  const signed = `${ts}.${payloadBytes.toString()}`;
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(sig);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

2. Defesa contra replay

  • Tolerância de tempo — recuse entregas cujo X-IORQ-Timestamp esteja a mais de 5 minutos do horário do servidor (já incluída no exemplo acima).
  • Deduplicação — a entrega é at-least-once e o payload não traz um event_id; deduplique pelo conteúdo de data (ex.: entity_id + status + nosso_numero). Ver Idempotência e reentrega.

3. Headers custom (camada adicional)

Os headers registrados no webhook continuam sendo reenviados em cada entrega. Use-os como segunda barreira (ex.: um header secreto validado no seu lado), mas trate a assinatura HMAC como a defesa autoritativa — headers custom não cobrem integridade do corpo nem replay.

4. TLS e certificado

  • A URL registrada precisa ser HTTPS — URLs http:// são rejeitadas na criação e na atualização do webhook.
  • Certificado válido emitido por CA pública. Mínimo TLS 1.2; TLS 1.3 recomendado.

5. Rotação do signing_secret

Ainda não há endpoint de rotação de secret. Em caso de suspeita de comprometimento, registre um novo webhook (que recebe um secret novo) e desative o antigo via PATCH com {"status": "INACTIVE"} — ou contate a IORQ.

6. O que responder no webhook

HTTPQuando devolverComportamento IORQ
2xxRecebido/processadoMarca como entregue, não reenvia
4xx / 5xx / timeoutFalha no seu ladoReenvia com backoff (até o limite de tentativas — ver Idempotência)

7. Próximos passos