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

# assinando requisições

> validar que as notificações vêm realmente da teceo usando HMAC-SHA256

se você configurou um **signing secret** no webhook, cada notificação chega assinada. nós usamos essa chave para criar um hash `HMAC-SHA256` pra você validar que o evento veio realmente da gente.

## como funciona

1. você configura um secret e salva em algum lugar no seu sistema onde você possa acessar pra validar as requisições
2. a gente calcula um hash `HMAC-SHA256` e enviamos no header `X-Webhook-Signature`
3. você recalcula o mesmo hash com o secret que você tem salvo
4. você compara os dois hashes e se bater, é porque a requisição é legítima

## headers que recebe

<ResponseField name="X-Webhook-Signature" type="string" required>
  hash HMAC-SHA256 do payload assinado, no formato `sha256=` seguido do valor em hexadecimal (ex: `sha256=abc123def456...`).
</ResponseField>

<ResponseField name="X-Webhook-Timestamp" type="string" required>
  timestamp ISO 8601 do evento, usado para evitar ataques de replay.
</ResponseField>

## como calcular o hash

o hash é calculado com a concatenação de `timestamp.body`:

<Card title="o valor assinado é construído assim">
  ```javascript Typescript theme={null}
  const payload = `${timestamp}.${body}`;
  const hash = HMAC_SHA256({ secret: "sua-secret", payload });
  ```

  **por que timestamp + body?** isso evita ataques de replay — o timestamp tem validade limitada, então mesmo que alguém capture uma requisição, não consegue reenviá-la depois.
</Card>

## validar a assinatura

<CodeGroup>
  ```javascript Typescript theme={null}
  const crypto = require('crypto');
  function validateSignature(body, timestamp, signature, secret) {
    // cria a string assinada: timestamp.body
    const payload = timestamp + '.' + JSON.stringify(body);
    // calcula HMAC-SHA256
    const hash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
    // compara com o header recebido
    const expected = 'sha256=' + hash;
    return signature === expected;
  }
  app.post('/webhooks', (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];
    // valida a assinatura
    if (!validateSignature(req.body, timestamp, signature, 'seu-secret')) {
    return res.status(401).json({ error: 'invalid signature' });
    }
    // processa webhook
    res.status(200).json({ ok: true });
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import json

  def validate_signature(body, timestamp, signature, secret):
    # cria a string assinada: timestamp.body
    if isinstance(body, dict):
      body_str = json.dumps(body, separators=(',', ':'))
    else:
      body_str = body

    payload = f"{timestamp}.{body_str}"

    # calcula HMAC-SHA256
    expected_hash = hmac.new(
      secret.encode(),
      payload.encode(),
      hashlib.sha256
    ).hexdigest()

    expected_sig = 'sha256=' + expected_hash
    return signature == expected_sig

  @app.post('/webhooks')
  async def webhook(request: Request):
    body = await request.json()
    signature = request.headers.get('x-webhook-signature')
    timestamp = request.headers.get('x-webhook-timestamp')

    # valida a assinatura
    if not validate_signature(body, timestamp, signature, 'seu-secret'):
      return {"error": "invalid signature"}, 401

    # processa webhook
    return {"ok": True}
  ```

  ```go Go theme={null}
  import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
  )

  func validateSignature(body []byte, timestamp, signature, secret string) bool {
    // cria a string assinada: timestamp.body
    payload := fmt.Sprintf("%s.%s", timestamp, string(body))

    // calcula HMAC-SHA256
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(payload))
    hash := "sha256=" + hex.EncodeToString(h.Sum(nil))

    return hash == signature
  }

  router.POST("/webhooks", func(c *gin.Context) {
    body, _ := c.GetRawData()
    signature := c.GetHeader("X-Webhook-Signature")
    timestamp := c.GetHeader("X-Webhook-Timestamp")

    // valida a assinatura
    if !validateSignature(body, timestamp, signature, "seu-secret") {
      c.JSON(401, gin.H{"error": "invalid signature"})
      return
    }

    // processa webhook
    c.JSON(200, gin.H{"ok": true})
  })
  ```
</CodeGroup>

## importante

* **use o corpo bruto** (raw): se o seu framework parseia o JSON automaticamente (ex: Express com `express.json()`), reconverta com `JSON.stringify(body)` antes de calcular o hash. se você tiver o raw body como string, use diretamente sem stringify
* **a ordem importa**: é sempre `timestamp` + `.` + `body`, nessa sequência
* **timestamp vem do header**: sempre use `X-Webhook-Timestamp`, nunca tente adivinhar ou usar a hora do seu servidor

## segurança

* **sempre valide**: mesmo se parecer desnecessário
* **nunca commite o secret** no controle de versão
* **guarde em variável de ambiente** ou serviço de secrets
* **sem validação = risco**: qualquer um pode se passar pela plataforma
