Skip to main content
Quando um webhook tem 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 prefixo v1= é a versão do esquema e não faz parte do que é assinado.
O x-idempotency-key não é opcional para quem valida a assinatura: ele é a única fonte de topic, partition e offset, que entram no cálculo.

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):
O corpo entra literalmente, byte a byte como foi enviado — sem reserialização, sem reordenar chaves, sem remover espaços.
Calcular o HMAC apenas sobre o corpo não funciona. O corpo é só o último dos cinco campos.

Se o corpo chegar comprimido

Requisições com 1024 bytes ou mais são enviadas com content-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 este secret e este corpo, o v1 do header é reproduzível. Secret devolvido pela API (exemplo, não é um secret real):
Requisição recebida:
String canônica montada a partir dela — t vem do x-webhook-signature, e topic, partition e offset vêm do x-idempotency-key:
Conferência no terminal:

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

Compare t 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

O secret é devolvido apenas na resposta de criação ou de atualização com signing: true. O GET não o retorna.
Todo PATCH com signing: true gera um secret novo e invalida o anterior — inclusive quando a assinatura já estava habilitada e a sua intenção era só mudar outro campo no mesmo request. Se você não quer rotacionar, omita o campo signing.
Depois de rotacionar, valide com o secret da última resposta recebida.

Se a assinatura não fecha

  1. Você está assinando os cinco campos, e não só o corpo?
  2. O separador é \n puro, sem \r, e não há quebra de linha no fim?
  3. O corpo é o cru, sem reserialização do seu framework?
  4. Se veio content-encoding: gzip, você descomprimiu antes?
  5. topic, partition e offset saíram do x-idempotency-key desta mesma requisição?
  6. O secret é o da última resposta com signing: true?