signing: true, toda requisição que o Omni Z-API envia ao seu endpoint carrega uma assinatura HMAC-SHA256. Esta página descreve exatamente o que é assinado, para que você reproduza o cálculo no seu servidor.
Vale para os dois tipos de webhook — de canal e de template.
Headers recebidos
t— instante da assinatura, em segundos desde a época (UTC).v1— o HMAC-SHA256 em hexadecimal minúsculo. O prefixov1=é a versão do esquema e não faz parte do que é assinado.
O que é assinado
A assinatura é calculada sobre uma string canônica de cinco campos separados por\n (byte 0x0A, sem \r, sem quebra de linha no final):
Se o corpo chegar comprimido
Requisições com 1024 bytes ou mais são enviadas comcontent-encoding: gzip. A assinatura é calculada sobre os bytes descomprimidos.
Se o seu framework HTTP já descomprime o corpo automaticamente, use o corpo que ele entrega. Se você lê o corpo cru, descomprima antes de calcular o HMAC — senão a validação passa nos payloads pequenos e falha nos grandes, o que aparenta ser intermitência.
Exemplo completo
Todos os valores abaixo são consistentes entre si: com estesecret e este corpo, o v1 do header é reproduzível.
Secret devolvido pela API (exemplo, não é um secret real):
t vem do x-webhook-signature, e topic, partition e offset vêm do x-idempotency-key:
Validando no seu servidor
Leia o corpo cru. Frameworks que fazem
JSON.parse e depois JSON.stringify antes de você assinar mudam os bytes (ordem de chaves, espaços, escapes) e a assinatura não fecha. No Express, use express.raw({ type: 'application/json' }) na rota do webhook.Proteção contra replay
Comparet com a hora atual do seu servidor e rejeite requisições fora de uma janela aceitável — 5 minutos é um valor razoável. Só isso não basta: use também o x-idempotency-key para descartar reentregas do mesmo evento, que chegam com o mesmo valor.
Rotação do secret
Osecret é devolvido apenas na resposta de criação ou de atualização com signing: true. O GET não o retorna.
Depois de rotacionar, valide com o secret da última resposta recebida.
Se a assinatura não fecha
- Você está assinando os cinco campos, e não só o corpo?
- O separador é
\npuro, sem\r, e não há quebra de linha no fim? - O corpo é o cru, sem reserialização do seu framework?
- Se veio
content-encoding: gzip, você descomprimiu antes? topic,partitioneoffsetsaíram dox-idempotency-keydesta mesma requisição?- O secret é o da última resposta com
signing: true?