EagleIX
Webhooks

Seguranca e Assinatura

Como verificar a autenticidade dos webhooks recebidos usando HMAC-SHA256

Como funciona

Na hora de cadastrar o webhook, voce envia o secret que voce escolheu. Quando a gente te notificar, a gente assina o payload com esse mesmo secret. O header X-Signature e incluido em toda entrega para que voce possa validar a autenticidade.

Headers enviados na entrega

Cada POST para seu endpoint inclui os seguintes headers:

HeaderDescricaoExemplo
X-SignatureAssinatura HMAC-SHA256sha256=abc123def456...
X-Webhook-EventTipo do eventoATUALIZACAO_STATUS_PIX
X-Webhook-IdUUID do webhook550e8400-...
X-Event-IdUUID do evento770e8400-...
X-TimestampEpoch seconds do envio1714564800
X-Trace-IdID de rastreamentoabc123
Content-TypeTipo do conteudoapplication/json

Verificando a assinatura

Para validar que a notificacao veio de nos:

  1. Pegue o header X-Timestamp
  2. Concatene com ponto (.) e o body raw: {X-Timestamp}.{body}
  3. Calcule HMAC-SHA256 usando o seu proprio secret
  4. Compare o resultado com o header X-Signature
input = "{X-Timestamp}.{body}"
assinatura_esperada = "sha256=" + hex(HMAC-SHA256(seu_secret, input))
// Compare assinatura_esperada com o header X-Signature

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public boolean verificarAssinatura(String secret, String body,
                                    String timestamp, String assinaturaRecebida) {
    String input = timestamp + "." + body;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] hash = mac.doFinal(input.getBytes(StandardCharsets.UTF_8));
    String assinaturaCalculada = "sha256=" + bytesToHex(hash);
    return assinaturaCalculada.equals(assinaturaRecebida);
}

Node.js

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

function verificarAssinatura(secret, body, timestamp, assinaturaRecebida) {
  const input = `${timestamp}.${body}`;
  const assinaturaCalculada =
    "sha256=" + createHmac("sha256", secret).update(input).digest("hex");

  return timingSafeEqual(
    Buffer.from(assinaturaCalculada),
    Buffer.from(assinaturaRecebida),
  );
}

Python

import hmac
import hashlib

def verificar_assinatura(secret: str, body: str, timestamp: str, assinatura_recebida: str) -> bool:
    input_data = f"{timestamp}.{body}"
    assinatura_calculada = "sha256=" + hmac.new(
        secret.encode(), input_data.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(assinatura_calculada, assinatura_recebida)

Sempre use comparacao em tempo constante (timingSafeEqual, hmac.compare_digest) para evitar ataques de timing.

Protecao contra replay attacks

Valide o header X-Timestamp para rejeitar entregas antigas:

  1. Extraia o valor de X-Timestamp (epoch seconds)
  2. Compare com o horario atual do servidor
  3. Rejeite se a diferenca for maior que 5 minutos
const MAX_AGE_SECONDS = 300; // 5 minutos
const agora = Math.floor(Date.now() / 1000);
const timestamp = parseInt(headers["x-timestamp"]);

if (Math.abs(agora - timestamp) > MAX_AGE_SECONDS) {
  // Rejeitar — possivel replay attack
  return res.status(401).send("Timestamp expirado");
}

Validacao de URL

Ao registrar ou atualizar um webhook, a URL e validada:

  • Deve usar o esquema HTTPS (HTTP nao e aceito)
  • Deve resolver para um IP publico
  • IPs bloqueados: loopback (127.0.0.1), link-local (169.254.x.x), redes privadas (10.x.x.x, 172.16-31.x.x, 192.168.x.x), CGNAT (100.64.0.0/10)
  • Servidores de metadata bloqueados: 169.254.169.254, metadata.google.internal

URLs invalidas retornam 422 Unprocessable Entity.


Secret

A secret e uma chave que voce gera e nos envia no momento do cadastro do webhook. Nos usamos essa chave para assinar cada notificacao enviada, e voce usa a mesma chave para verificar que a notificacao realmente veio de nos.

A secret nao e devolvida apos o cadastro. Guarde-a de forma segura no seu sistema antes de enviar.

Validacoes

  • Minimo 32 caracteres, maximo 500 caracteres
  • Deve ser uma string aleatoria (como um hash) para garantir seguranca
  • Armazenamos de forma criptografada — nunca em texto puro

Como gerar

openssl rand -hex 32

Isso gera uma string hexadecimal de 64 caracteres, segura para uso como secret.


Payload recebido pelo integrador

O body do POST enviado ao seu endpoint e um array JSON com os eventos:

[
  {
    "numeroDocumento": "12345678901",
    "status": "SETTLED",
    "valor": 15000,
    "end2end": "E12345678202301011200abcdef12345",
    "razaoRejeicao": null
  }
]
CampoTipoObrigatorioDescricao
numeroDocumentostringsimNumero do documento (CPF/CNPJ)
statusstringsimStatus da transacao PIX
valorlongsimValor em centavos
end2endstringnaoIdentificador end-to-end da transacao
razaoRejeicaoobjetonaoMotivo da rejeicao (presente quando status = REJECTED)
razaoRejeicao.codigostringsim*Codigo do erro (ex: BE08)
razaoRejeicao.descricaostringsim*Descricao do erro

Os campos de razaoRejeicao sao obrigatorios quando o objeto esta presente.

On this page