# Validar a assinatura do webhook (HMAC SHA-256)

> Confirme que cada entrega de webhook veio mesmo do GS Engage recalculando a assinatura HMAC SHA-256 a partir do corpo cru e do seu secret.

- URL canônica: https://docs.growthstation.app/docs/api/guias/validar-assinatura-webhook
- Idioma: pt-BR
- Última atualização: 2026-09-20T08:21:20.450Z
- Produto: GS Engage
- Mantido por: Produto GS Engage



<PageHero emoji="🔐" title="Validar a assinatura do webhook" description="Antes de confiar em qualquer aviso que chega na sua URL, confirme que ele veio mesmo do GS Engage." gradient="brand" />

🎯 O que você quer garantir [#-o-que-você-quer-garantir]

Você configurou um webhook e agora o GS Engage envia um POST para a sua URL toda vez que algo acontece — uma prospecção foi ganha, uma ligação terminou, uma atividade foi concluída. Um **webhook** é só isso: um aviso automático que o GS Engage manda para o seu sistema.

O problema é que a sua URL fica exposta na internet. Qualquer pessoa que descobrir o endereço pode mandar um POST fingindo ser o GS Engage. Se o seu sistema confiar cegamente, alguém poderia inventar uma "venda ganha" que nunca existiu.

A validação de assinatura resolve isso. Cada entrega chega **assinada**, e só quem tem o seu `secret` consegue produzir uma assinatura válida. Você recalcula essa assinatura do seu lado e compara. Se bater, o aviso é autêntico. Se não bater, você descarta.

<Callout type="tip" title="Onde isso se encaixa">
  Este guia é o passo seguinte depois de você criar o webhook. Se ainda não criou, comece em [Criar e assinar um webhook](/docs/api/guias/avisos-de-venda-webhook) e volte aqui com o seu `secret` em mãos.
</Callout>

🧠 O que é HMAC SHA-256, em palavras simples [#-o-que-é-hmac-sha-256-em-palavras-simples]

**HMAC** (do inglês *Hash-based Message Authentication Code*, código de autenticação de mensagem) é uma forma de "carimbar" um texto usando uma senha secreta compartilhada.

Pense assim:

* O GS Engage pega o **corpo da mensagem** (o JSON que ele vai te enviar) e o seu **`secret`**.
* Ele passa os dois por uma função matemática — o **SHA-256** — que produz um código curto e único: a **assinatura**.
* Esse código viaja junto com a entrega, dentro de um cabeçalho.

Do seu lado, você faz **exatamente a mesma conta**: pega o corpo que chegou, junta o seu `secret` e gera a sua própria assinatura. Como só você e o GS Engage conhecem o `secret`, só vocês dois conseguem produzir o mesmo código.

<Mermaid
  chart={`graph LR
A["Corpo da entrega (JSON cru)"] --> C["HMAC SHA-256"]
B["Seu secret"] --> C
C --> D["Assinatura calculada"]
E["Assinatura recebida no cabeçalho"] --> F{"São iguais?"}
D --> F
F -->|Sim| G["Entrega autêntica ✅"]
F -->|Não| H["Rejeitar 🚨"]`}
/>

<Callout type="info" title="De onde vem o secret">
  O `secret` é devolvido **uma única vez**, na resposta de criação do webhook (`POST /api/v1/webhooks`). Ele não aparece em nenhuma listagem depois. Se você não guardou, apague o webhook e crie outro para receber um `secret` novo.
</Callout>

🧩 O envelope que chega na sua URL [#-o-envelope-que-chega-na-sua-url]

Cada entrega é um POST com este formato de corpo:

```json
{
  "id": "evt_9f3c...",
  "test": false,
  "event": "prospection.won",
  "data": { },
  "retries": 0,
  "manualRetries": 0,
  "createdAt": "2026-07-17T14:02:11.000Z"
}
```

A assinatura viaja em um **cabeçalho** da requisição HTTP, separada do corpo. É esse valor que você vai comparar com o que calcular.

✅ Verificador pronto para copiar [#-verificador-pronto-para-copiar]

Os exemplos abaixo fazem três coisas: leem o **corpo cru**, recalculam a assinatura com o seu `secret` e comparam em **tempo constante**. Escolha a sua linguagem.

<Tabs items={['Node.js', 'Python']}>
  <Tab value="Node.js">
    Este exemplo usa o Express. O ponto crucial é capturar o corpo **como texto puro**, antes de qualquer parse de JSON.

    ```js
    const express = require("express");
    const crypto = require("crypto");

    const app = express();

    // Guarda o corpo CRU (Buffer) para poder recalcular a assinatura.
    app.use(
      express.json({
        verify: (req, res, buf) => {
          req.rawBody = buf;
        },
      })
    );

    const WEBHOOK_SECRET = process.env.GSENGAGE_WEBHOOK_SECRET;

    function assinaturaConfere(rawBody, assinaturaRecebida) {
      // Recalcula HMAC SHA-256 sobre o corpo cru + o seu secret.
      const esperada = crypto
        .createHmac("sha256", WEBHOOK_SECRET)
        .update(rawBody)
        .digest("hex");

      const a = Buffer.from(esperada);
      const b = Buffer.from(assinaturaRecebida || "");

      // Comparação em tempo constante: evita vazar dicas pelo tempo de resposta.
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }

    app.post("/webhooks/gsengage", (req, res) => {
      // Substitua pelo nome exato do cabeçalho de assinatura da entrega.
      const assinaturaRecebida = req.get("<cabecalho-de-assinatura>");

      if (!assinaturaConfere(req.rawBody, assinaturaRecebida)) {
        // Não bateu: descarte sem processar.
        return res.status(401).send("assinatura inválida");
      }

      const evento = req.body;
      console.log("Entrega autêntica:", evento.event, evento.id);

      // Responda rápido; processe o resto em segundo plano se for pesado.
      res.status(200).send("ok");
    });

    app.listen(3000);
    ```
  </Tab>

  <Tab value="Python">
    Este exemplo usa Flask. Repare que ele lê `request.get_data()` (o corpo cru) e **não** `request.json`.

    ```python
    import hmac
    import hashlib
    import os
    from flask import Flask, request

    app = Flask(__name__)

    WEBHOOK_SECRET = os.environ["GSENGAGE_WEBHOOK_SECRET"]

    def assinatura_confere(raw_body: bytes, assinatura_recebida: str) -> bool:
        # Recalcula HMAC SHA-256 sobre o corpo cru + o seu secret.
        esperada = hmac.new(
            WEBHOOK_SECRET.encode("utf-8"),
            raw_body,
            hashlib.sha256,
        ).hexdigest()

        # Comparação em tempo constante: evita vazar dicas pelo tempo de resposta.
        return hmac.compare_digest(esperada, assinatura_recebida or "")

    @app.post("/webhooks/gsengage")
    def receber_webhook():
        raw_body = request.get_data()  # corpo CRU, em bytes

        # Substitua pelo nome exato do cabeçalho de assinatura da entrega.
        assinatura_recebida = request.headers.get("<cabecalho-de-assinatura>")

        if not assinatura_confere(raw_body, assinatura_recebida):
            # Não bateu: descarte sem processar.
            return "assinatura inválida", 401

        evento = request.get_json()
        print("Entrega autêntica:", evento["event"], evento["id"])

        # Responda rápido; processe o resto depois se for pesado.
        return "ok", 200
    ```
  </Tab>
</Tabs>

Se a assinatura bater, você tem certeza de que o aviso é do GS Engage e pode processar o evento com segurança — atualizar o CRM, disparar uma notificação, o que fizer sentido para o seu negócio.

🚨 Os dois cuidados que não podem faltar [#-os-dois-cuidados-que-não-podem-faltar]

<Callout type="danger" title="Use o corpo CRU, nunca o JSON reparseado">
  A assinatura é calculada sobre os **bytes exatos** que chegaram. Se você transformar o corpo em objeto e depois em texto de novo (`JSON.stringify`, `json.dumps`), o resultado quase sempre fica diferente do original — a ordem das chaves, os espaços e a formatação mudam. Aí a conta não bate mesmo sendo uma entrega legítima. Sempre valide contra o corpo cru (`req.rawBody`, `request.get_data()`) e só depois faça o parse.
</Callout>

<Callout type="warning" title="Se não bater, rejeite — sem exceção">
  Uma assinatura que não confere significa uma de duas coisas: a mensagem foi adulterada no caminho, ou não veio do GS Engage. Nos dois casos, responda com **401** e **não processe nada**. Nunca "deixe passar por via das dúvidas": um único evento falso aceito pode sujar seus dados ou disparar uma ação indevida.
</Callout>

✅ Checklist do verificador [#-checklist-do-verificador]

<Checklist id="webhook-hmac-check" title="Antes de ir para produção">
  <ChecklistItem>
    Guardei o 

    `secret`

     que veio na criação do webhook, em variável de ambiente.
  </ChecklistItem>

  <ChecklistItem>
    Meu servidor captura o 

    **corpo cru**

     antes de qualquer parse de JSON.
  </ChecklistItem>

  <ChecklistItem>
    Recalculo a assinatura com 

    **HMAC SHA-256**

     usando esse corpo cru e o 

    `secret`

    .
  </ChecklistItem>

  <ChecklistItem>
    Comparo em 

    **tempo constante**

     (

    `timingSafeEqual`

     / 

    `hmac.compare_digest`

    ).
  </ChecklistItem>

  <ChecklistItem>
    Rejeito com 

    **401**

     e não processo quando a assinatura não confere.
  </ChecklistItem>
</Checklist>

🔄 A migração de SHA-1 para SHA-256 [#-a-migração-de-sha-1-para-sha-256]

A assinatura dos webhooks passou a ser calculada com **SHA-256**, no lugar do antigo **SHA-1**. O SHA-256 é o algoritmo recomendado hoje: mais robusto e sem as fraquezas conhecidas do SHA-1.

<DoDont>
  <DoDontItem type="do">
    Use 

    `sha256`

     na função de HMAC, exatamente como nos exemplos acima.
  </DoDontItem>

  <DoDontItem type="dont">
    Não deixe código antigo calculando 

    `sha1`

     — as assinaturas nunca vão bater e você vai rejeitar entregas legítimas.
  </DoDontItem>
</DoDont>

<Callout type="tip" title="Migrando um verificador antigo">
  Se o seu integrador já validava com SHA-1, a mudança é de uma linha: troque o algoritmo para `sha256`. O resto da lógica — corpo cru, comparação em tempo constante, rejeição em caso de falha — continua igual.
</Callout>

❓ Perguntas frequentes [#-perguntas-frequentes]

<FAQ>
  <FAQItem question="Perdi o meu secret. Como recupero?">
    Não há como recuperar: o `secret` só aparece na resposta de criação do webhook. Apague o webhook atual com `DELETE /api/v1/webhooks/{webhookId}` e crie um novo com `POST /api/v1/webhooks` para receber um `secret` novo.
  </FAQItem>

  <FAQItem question="A conta nunca bate, mesmo com o secret certo. O que testar primeiro?">
    Na quase totalidade dos casos é o corpo. Confirme que você está passando o **corpo cru** para o HMAC, e não o JSON reparseado. Um `console.log` do `rawBody` ajuda a ver se algum middleware está transformando o corpo antes de você validá-lo.
  </FAQItem>

  <FAQItem question="O que é o campo test do envelope?">
    Quando `test` é `true`, a entrega foi disparada como teste (não corresponde a um evento real de negócio). A assinatura é calculada da mesma forma — valide igual e trate o conteúdo como um ensaio.
  </FAQItem>

  <FAQItem question="Preciso de tudo isso se eu uso Zapier ou Make?">
    Não. As plataformas no-code que consomem os gatilhos de webhook (como o Zapier) cuidam do recebimento por você. A validação de assinatura entra em cena quando você recebe o webhook no **seu próprio servidor**, com código.
  </FAQItem>
</FAQ>

Artigos Relacionados [#artigos-relacionados]

<RelatedArticles>
  <RelatedArticle href="/docs/api/guias/avisos-de-venda-webhook" title="Criar e assinar um webhook" description="Registre a sua URL, escolha os eventos e guarde o secret." />

  <RelatedArticle href="/docs/api/webhooks" title="Eventos de webhook disponíveis" description="Conheça cada evento que o GS Engage pode enviar." />

  <RelatedArticle href="/docs/api/comece-aqui/quickstart" title="Comece aqui pela API" description="Autenticação, primeiros passos e boas práticas." />
</RelatedArticles>
