Skip to main content
A URL do seu endpoint é pública: qualquer pessoa que descubra o endereço consegue mandar um POST para ela. Por isso toda entrega da Zapfy vem assinada com um segredo que só você e a Zapfy conhecem. Verificar a assinatura garante três coisas:
  • Origem — a requisição foi enviada pela Zapfy.
  • Integridade — o corpo chegou exatamente como saiu, sem nenhum byte alterado.
  • Frescor — não é uma requisição antiga, capturada e reenviada (replay).
A verificação é opcional, mas recomendada para qualquer endpoint que dispare ação de negócio (responder cliente, criar pedido, atualizar CRM).
As entregas seguem a especificação aberta Standard Webhooks, a mesma usada por vários provedores de webhook. Na prática, você valida com a biblioteca oficial do padrão, sem escrever código de criptografia.

O secret

Cada webhook tem o seu próprio secret, gerado pela Zapfy na criação. Ele volta uma única vez, na resposta do POST que cria o webhook:
Guarde o secret assim que criar o webhook: ele não aparece de novo em nenhuma outra resposta da API. Se perder o valor, ou suspeitar que ele vazou, remova o webhook e crie outro — o novo vem com um secret novo.
Trate o secret como uma senha: variável de ambiente ou cofre de segredos, nunca no código-fonte nem em log. Com ele, qualquer um consegue forjar uma entrega válida.

Headers da entrega

O tipo do evento não tem header próprio: ele está no campo type do corpo, que é assinado.

Verificando com a biblioteca oficial

Instale a biblioteca do Standard Webhooks e passe a ela o corpo cru da requisição, os headers e o seu secret. Ela confere a assinatura, rejeita requisições com mais de 5 minutos e devolve o evento já parseado.
Há bibliotecas oficiais também para Go, Java, Ruby, Rust, C# e outras linguagens — veja a lista em standardwebhooks.com.

Verificando sem biblioteca

Se preferir não adicionar dependência, a verificação cabe em poucas linhas. A assinatura é um HMAC-SHA256 do ID, do timestamp e do corpo cru, separados por ponto. A chave é o secret sem o prefixo whsec_, decodificado de base64 — não a string em si:
  1. Leia o corpo cru da requisição, antes de qualquer parse de JSON.
  2. Leia webhook-id, webhook-timestamp e webhook-signature. Se faltar algum, rejeite.
  3. Rejeite se o timestamp estiver a mais de 5 minutos do seu relógio.
  4. Calcule a assinatura esperada como acima.
  5. O header pode trazer mais de uma assinatura, separadas por espaço. Aceite se qualquer uma delas for igual à esperada, usando comparação de tempo constante.
Node.js
Todos os três campos fazem parte do conteúdo assinado, então nenhum pode ser trocado sem invalidar a assinatura. Não adianta um atacante mudar o webhook-timestamp para “rejuvenescer” uma requisição capturada, nem mudar o webhook-id para escapar da sua deduplicação.

Proteção contra replay

A assinatura sozinha prova que a requisição saiu da Zapfy, mas não quando. Sem a checagem do timestamp, alguém que capturasse uma entrega válida poderia reenviá-la dias depois e ela passaria. A tolerância de 5 minutos fecha essa janela — a biblioteca oficial já faz essa checagem.
  • O webhook-timestamp é a hora do envio, carimbada a cada tentativa. Uma reentrega feita depois de uma falha chega com timestamp novo e passa na tolerância normalmente.
  • Ele não é o timestamp do corpo, que é a hora do evento e se mantém igual em todas as tentativas.
  • Mantenha o relógio do servidor sincronizado (NTP). Um relógio desviado alguns minutos passa a rejeitar entregas legítimas.
Para fechar também a janela dos 5 minutos, combine com a deduplicação pelo ID do evento (webhook-id, ou o id do corpo — são o mesmo valor), que você já precisa ter porque a entrega é at-least-once. Um replay dentro da janela carrega o ID de um evento já processado e é descartado; e como o ID é assinado, trocá-lo invalida a requisição.

Erros comuns

Quase sempre é o corpo. Se o framework fizer o parse do JSON e você verificar o resultado de JSON.stringify(req.body), os bytes mudam (espaços, ordem de chaves, escapes) e o HMAC é outro. Use o corpo cru, exatamente como chegou — no Express, express.raw; no Flask, request.get_data(); no PHP, php://input.
A chave do HMAC é o secret decodificado de base64, depois de remover o prefixo whsec_. Usar a string whsec_... direto como chave gera outra assinatura. A assinatura também é base64, não hex.
O webhook-timestamp está em segundos. Comparar com Date.now() (milissegundos) sem dividir por 1000 rejeita tudo. Confira também o relógio do servidor.
A comparação comum para no primeiro caractere diferente, e o tempo de resposta vaza quantos caracteres estavam certos. Use crypto.timingSafeEqual, hmac.compare_digest ou hash_equals — as bibliotecas oficiais já fazem isso.
Nomes de header não diferenciam maiúsculas de minúsculas. No Node eles chegam em minúsculo (req.headers["webhook-id"]); no PHP, em $_SERVER com o prefixo HTTP_ (HTTP_WEBHOOK_ID).

Testando seu endpoint

Para testar a verificação sem esperar um evento real, gere uma requisição assinada com o seu secret no terminal:
Seu endpoint deve responder 2xx. Altere um caractere do BODY depois de gerar a assinatura, ou use um TS de uma hora atrás, e ele deve responder 401.