> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verificar assinaturas de webhooks

> Todo webhook e toda solicitação de ferramenta do ThunderPhone são assinados. Verifique uma vez; reutilize em qualquer lugar.

Todas as solicitações que enviamos ao seu servidor — entregas de webhook e
invocações de endpoints de ferramentas — incluem uma assinatura HMAC-SHA256 no
cabeçalho `X-ThunderPhone-Signature`. Implemente a verificação corretamente uma vez e
use o mesmo auxiliar em todos os handlers.

## O algoritmo

1. Leia o corpo **bruto** da solicitação — os bytes exatos que enviamos para você via POST.
2. Calcule `hmac_sha256(secret, body).hexdigest()`.
3. Compare em **tempo constante** com `X-ThunderPhone-Signature`.
   (Uma comparação ingênua de strings expõe informações de temporização.)

Assinamos exatamente os bytes que transmitimos, portanto verificar o corpo bruto
sempre funciona. Esses bytes também são a **serialização JSON canônica**
do payload — chaves ordenadas alfabeticamente, separadores compactos
(`,` e `:` sem espaços), UTF-8. Isso oferece uma segunda receita totalmente
equivalente quando seu framework expõe apenas JSON analisado:
serialize novamente de forma canônica e calcule o HMAC disso.

```python theme={null}
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
```

Prefira o corpo bruto — é uma etapa a menos e evita peculiaridades de
conversão de números JSON em algumas linguagens.

## Qual segredo?

| Origem                                                                                           | Segredo                                                                                                                 |
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| [Endpoint de webhook](/pt/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                | `secret` por endpoint (48 caracteres hexadecimais), retornado uma vez na criação                                        |
| [Webhook legado de URL única](/api-reference/organizations#legacy-single-url-webhook)            | `secret` por organização retornado em `GET /v1/webhook`                                                                 |
| [Invocação de endpoint de ferramenta](/pt/tools/overview) (chamada direta ao seu `endpoint.url`) | O **segredo de webhook no nível da organização** (o mesmo do webhook legado de URL única) — não um segredo por endpoint |

Armazene o segredo no seu gerenciador de segredos ou em uma variável de ambiente — nunca faça commit dele.

## Implementações de referência

Todas as quatro verificam o corpo bruto da solicitação:

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac


  def verify(body: bytes, signature: str, secret: str) -> bool:
      """Constant-time HMAC-SHA256 verification."""
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature or "")
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  export function verify(body, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    if (!signature || expected.length !== signature.length) return false;
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature),
    );
  }
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
  )

  func Verify(body []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(signature))
  }
  ```

  ```ruby Ruby theme={null}
  require "openssl"

  def verify(body, signature, secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
    Rack::Utils.secure_compare(expected, signature.to_s)
  end
  ```
</CodeGroup>

## Configuração específica do framework

<CodeGroup>
  ```python FastAPI theme={null}
  from fastapi import FastAPI, HTTPException, Request

  app = FastAPI()

  @app.post("/thunderphone-webhook")
  async def hook(request: Request):
      body = await request.body()           # raw bytes, NOT request.json()
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          raise HTTPException(status_code=401)

      import json
      event = json.loads(body)
      # … dispatch on event["type"] …
      return {"ok": True}
  ```

  ```javascript Express theme={null}
  import express from "express";

  const app = express();

  app.post(
    "/thunderphone-webhook",
    // IMPORTANT: parse as raw; do NOT use express.json() here.
    express.raw({ type: "application/json" }),
    (req, res) => {
      const sig = req.header("X-ThunderPhone-Signature") || "";
      if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
        return res.sendStatus(401);
      }
      const event = JSON.parse(req.body.toString("utf8"));
      // … dispatch on event.type …
      res.sendStatus(204);
    },
  );
  ```

  ```python Django theme={null}
  import json

  from django.http import JsonResponse, HttpResponseForbidden
  from django.views.decorators.csrf import csrf_exempt
  from django.views.decorators.http import require_POST


  @csrf_exempt
  @require_POST
  def hook(request):
      body = request.body  # raw bytes
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          return HttpResponseForbidden("invalid signature")
      event = json.loads(body)
      # … dispatch on event["type"] …
      return JsonResponse({"ok": True})
  ```
</CodeGroup>

## Verificação de chamadas de ferramenta

Quando o agente invoca diretamente uma das suas
[ferramentas de função](/pt/tools/overview) (a ferramenta tem um
`endpoint`), a solicitação inclui dois cabeçalhos do ThunderPhone junto
com os `endpoint.headers` configurados:

* `X-ThunderPhone-Call-ID` — o ID numérico da chamada ao vivo.
* `X-ThunderPhone-Signature` — HMAC-SHA256, usando como chave o seu
  **segredo de webhook no nível da organização**, sobre os bytes exatos
  do corpo da solicitação.

O mesmo auxiliar `verify()` funciona sem alterações, com duas particularidades:

1. Ferramentas **`GET` / `DELETE` não têm corpo.** Os argumentos são enviados como
   parâmetros de consulta, e a assinatura é calculada sobre a **string de bytes
   vazia** — portanto, `verify(b"", sig, secret)` (Python) ou
   `verify(Buffer.alloc(0), sig, secret)` (Node). **Não** gere hash da
   string de consulta.
2. **Organizações sem um webhook legado configurado não têm um segredo da organização.**
   Nesse caso, as chamadas de ferramenta incluem apenas `X-ThunderPhone-Call-ID` e nenhum
   cabeçalho de assinatura. Configure o webhook legado
   (`PUT /v1/webhook`) para obter um segredo de assinatura ou autentique as chamadas de
   ferramenta com seu próprio cabeçalho via `endpoint.headers`.

```python theme={null}
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...
```

O envio de ferramentas no **modo** webhook (ferramentas sem um `endpoint`, entregues
ao webhook da sua organização como `telephony.tool` / `web.tool`) é um webhook
comum assinado — a receita padrão acima se aplica. Consulte
[Ferramentas de função](/pt/tools/overview) para ver os dois formatos de solicitação.

## Erros comuns

<AccordionGroup>
  <Accordion title="Resserializar com a formatação padrão">
    Analisar o corpo e serializá-lo novamente com os padrões da sua biblioteca JSON
    (espaços após `,` / `:`, chaves na ordem de inserção) produz
    bytes diferentes e invalida o HMAC. Verifique o corpo bruto — ou, se
    precisar resserializá-lo, corresponda exatamente ao nosso formato canônico: chaves
    ordenadas, separadores compactos, UTF-8.
  </Accordion>

  <Accordion title="O framework analisa JSON automaticamente">
    O middleware `express.json()` do Express consome o fluxo do corpo
    e você perde os bytes brutos. Use `express.raw()` especificamente na rota
    do webhook ou armazene o corpo bruto em buffer em um pré-middleware.
    O mesmo vale para NestJS / Koa — consulte a documentação sobre "corpo bruto".
  </Accordion>

  <Accordion title="Comparação insegura em relação ao tempo">
    `expected === signature` em JS ou `expected == signature` em
    Python são comparações com tempo variável. Use `crypto.timingSafeEqual`
    ou `hmac.compare_digest`, respectivamente. A diferença de desempenho
    é nula.
  </Accordion>

  <Accordion title="Segredo incorreto para endpoints de ferramentas">
    Chamadas diretas a endpoints de ferramentas são assinadas com o **segredo de webhook
    no nível da organização** (`GET /v1/webhook`) — não com qualquer segredo por endpoint
    de `/v1/developer/webhook-endpoints`. Reutilize a mesma função `verify()`,
    mas certifique-se de fornecer a ela o segredo da organização nas rotas de ferramentas.
  </Accordion>

  <Accordion title="Gerar hash da string de consulta em ferramentas GET/DELETE">
    Para métodos de ferramentas sem corpo, a assinatura abrange a string de bytes
    vazia, mantendo uma única receita universal: aplique HMAC ao corpo bruto da solicitação,
    seja ele qual for. Gerar hash da URL ou da string de consulta nunca corresponderá.
  </Accordion>

  <Accordion title="Não retornar 401 em caso de divergência">
    Retornar 200 quando a verificação falha transforma o manipulador em um alvo de
    repetição. Sempre responda com um código diferente de 2xx se a verificação falhar.
  </Accordion>
</AccordionGroup>

***

## Próximas etapas

<CardGroup cols={2}>
  <Card title="Visão geral dos webhooks" icon="bolt" href="/pt/webhooks/overview">
    Semântica de entrega, novas tentativas, IPs de origem.
  </Card>

  <Card title="Endpoints de webhook" icon="plug" href="/pt/webhooks/endpoints">
    Gerencie várias URLs, alterne segredos.
  </Card>

  <Card title="Ferramentas de função" icon="screwdriver-wrench" href="/pt/tools/overview">
    Os dois caminhos de invocação de ferramentas e os formatos das solicitações.
  </Card>

  <Card title="Integrações de ferramentas" icon="wrench" href="/pt/guides/build-tool-integration">
    Crie uma integração completa com ferramentas, de ponta a ponta.
  </Card>
</CardGroup>
