Verificação de assinatura de webhook
Este guia explica como a Fluxis assina webhooks de saída e como verificar que (1) a requisição veio da Fluxis e (2) o corpo JSON não foi alterado.
Referência de implementação: src/pkg/utils/crypto_signature.go (GenerateRequestSignature, VerifyRequestSignature, toCanonicalJSON).
Relacionado: Visão geral de webhooks · Guia de integração API
::::note Webhooks em nível de conta
URLs de webhook e segredos de assinatura estão vinculados a uma conta (POST /v1/account/{accountID}/webhook), não a Pontos de Venda individuais. Eventos de pagamento incluem point_of_sale_id no payload quando relevante.
::::
O que a verificação comprova
| Propriedade | Significado |
|---|---|
| Autenticidade | Apenas quem tem seu secret de webhook poderia produzir uma assinatura válida |
| Integridade | O corpo JSON não foi modificado em trânsito |
Rejeite requisições que falharem na verificação. Não processe a entrega do pagamento até que assinatura e timestamp passem nas checagens.
Pré-requisitos
- Segredo de webhook — Retornado uma vez na resposta de
POST /v1/account/{accountID}/webhook. Armazene com segurança; não pode ser recuperado depois. - Endpoint HTTPS — Seu servidor deve aceitar
POSTcomContent-Type: application/json. - Corpo parseado — Faça parse do JSON primeiro, depois re-canonicalize para assinar. Nunca assine bytes brutos da requisição.
Algoritmo
A Fluxis usa HMAC-SHA256 sobre uma string determinística construída a partir do timestamp e do corpo do webhook:
Code
Cabeçalhos de entrega:
| Cabeçalho | Valor |
|---|---|
X-Request-Signature | HMAC-SHA256 codificado em hexadecimal |
X-Request-Timestamp | Segundos Unix (string, ex.: "1712345678") |
Content-Type | application/json |
Validação de timestamp: Rejeite se now - timestamp > 300 segundos (WEBHOOK_MAX_AGE_SECONDS). Timestamps futuros não são rejeitados na implementação atual do servidor — você pode adicionar checagens mais rígidas no handler, se desejar.
Objeto assinado
Assine o corpo completo do webhook como objeto JSON, tipicamente:
Code
Não assine apenas data, e não assine a string do corpo HTTP bruto antes do parse.
Regras de JSON canônico
A Fluxis constrói JSON canônico via toCanonicalJSON:
- Serializa o valor em JSON (tags de struct e
omitemptyaplicam — campos nil/zero são omitidos). - Deserializa em
map[string]any/ arrays para que objetos aninhados virem maps. - Serializa novamente —
json.Marshaldo Go em maps produz JSON compacto (sem espaços) com chaves ordenadas alfabeticamente em cada nível de objeto.
Seu verificador deve reproduzir esse comportamento:
| Regra | Detalhe |
|---|---|
| Compacto | Sem espaços: {"a":1} não { "a": 1 } |
| Chaves ordenadas | Em cada nível de objeto, incluindo data aninhado |
| Como recebido | Campos opcionais ausentes no payload devem permanecer ausentes na forma canônica |
| Não bytes brutos | Middleware que reformata JSON antes de verificar quebra assinaturas |
::::note omitempty
Se external_reference_id ou account_id não estiver definido, essas chaves são omitidas do JSON que a Fluxis assina. Reconstruir o corpo a partir de um esquema fixo que sempre inclui essas chaves produzirá assinatura diferente.
::::
Checklist de verificação
- Leia
X-Request-SignatureeX-Request-Timestampdos cabeçalhos. - Faça parse do corpo da requisição em um objeto genérico (
map/dict). - Rejeite se
now - timestamp > 300. - Serialize o objeto parseado em JSON canônico (chaves ordenadas, compacto).
- Monte
signed_string = timestamp + "." + canonical_json. - Calcule
HMAC-SHA256(secret, signed_string), codifique em hex e compare com igualdade de tempo constante (após checagem de tamanho).
Exemplo prático
Use estes valores fixos para testar sua implementação:
| Entrada | Valor |
|---|---|
| Secret | whsec_example_for_documentation_only |
| Timestamp | 1712345678 |
| Body | Veja JSON abaixo |
Corpo (objeto parseado):
Code
JSON canônico (uma linha):
Code
String assinada:
Code
Assinatura esperada (hex):
Code
Se sua implementação produzir essa assinatura para as entradas acima, ela coincide com a assinatura de produção da Fluxis.
Exemplos de código
Node.js / TypeScript
Code
::::warning timingSafeEqual no Node.js
crypto.timingSafeEqual lança erro se os tamanhos dos buffers diferirem. Compare tamanhos primeiro (como acima), ou use hmac.compare_digest em Python.
::::
Python
Code
Go
Corresponde a toCanonicalJSON e VerifyRequestSignature de produção:
Code
Testar seu handler
- Registre um webhook:
POST /v1/account/{accountID}/webhookcom sua URL pública eevent_type: "payment_request". - Salve o
secretda resposta de criação. - Chame
POST /v1/account/{accountID}/webhook/{webhookID}/test— a Fluxis faz POST de um payload de amostra assinado na sua URL usando o mesmo caminho de código da produção (GenerateRequestSignatureem{ "data": … }). - Verifique a assinatura no handler e retorne HTTP
200. - Opcionalmente compare com o cálculo manual usando o exemplo prático acima.
Veja tentativas de entrega em GET /v1/account/{accountID}/webhook/logs?page=1&page_size=20.
Falhas comuns
| Sintoma | Causa provável |
|---|---|
| Assinatura sempre inválida | Segredo incorreto (não o da criação); assinar bytes brutos em vez de JSON parseado |
| Inválida intermitente | Campos opcionais adicionados/removidos vs o que foi assinado; middleware alterando req.body |
| Assinatura válida, rejeitada mesmo assim | Diferença de relógio além de 300 s; usar timestamp errado (deve coincidir com o cabeçalho X-Request-Timestamp) |
| Handler Node falha na comparação | timingSafeEqual chamado com strings hex de tamanhos diferentes — adicione checagem de tamanho primeiro |
| Divergência em Go com structs aninhados | Assinar struct sem round-trip por any — use o padrão canonicalJSON acima |
| Teste funciona, produção falha | Payload de produção sem campos que seu vetor de teste sempre inclui |
Notas de segurança
- Use comparação de tempo constante (
hmac.Equal,hmac.compare_digest,timingSafeEqualcom buffers de mesmo tamanho). - Rejeite timestamps obsoletos (mais antigos que 300 segundos).
- Processe a lógica de negócio somente depois que a verificação de assinatura for bem-sucedida.
- Armazene o segredo de webhook em um gerenciador de segredos, não no controle de versão.
- Use HTTPS no endpoint; trate o segredo como credencial de API.
Precisa de ajuda?
Entre em contato em [email protected] com uma cópia redigida do corpo recebido, cabeçalhos (omita o segredo) e seu JSON canônico calculado se a verificação falhar.