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

# Segurança

> Confira a assinatura de cada entrega, descarte repetição e entenda a retentativa.

Todo `POST` chega assinado. Confira a assinatura antes de confiar no corpo: sem isso, quem descobrir
a URL do seu endpoint consegue enviar um desfecho forjado.

## Header

```http theme={null}
X-Legitimuz-Signature: t=1757865000,v1=5f2c8a1e...
```

| Parte | O que é                                    |
| ----- | ------------------------------------------ |
| `t`   | o instante da assinatura, em segundos Unix |
| `v1`  | o HMAC-SHA256 em hexadecimal               |

O HMAC é calculado com o segredo do endpoint sobre o timestamp, um ponto, e o corpo cru:

```text theme={null}
${timestamp}.${corpo cru}
```

<Warning>
  Use o corpo **cru**, byte a byte, antes de qualquer parse. Reserializar o JSON muda os bytes e a
  conferência falha.
</Warning>

## Como conferir

1. Extraia `t` e `v1` do header.
2. Recuse se `t` estiver a mais de cinco minutos do relógio do seu servidor.
3. Recalcule o HMAC sobre `${t}.${corpo cru}` com o segredo do endpoint.
4. Compare em tempo constante.

<CodeGroup>
  ```ts title="Node.js" theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function assinaturaValida(corpoCru: string, header: string, segredo: string): boolean {
    const partes = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
    const timestamp = Number(partes.t);
    if (!timestamp || !partes.v1) return false;
    if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;

    const esperada = createHmac("sha256", segredo).update(`${timestamp}.${corpoCru}`).digest("hex");
    const a = Buffer.from(esperada, "utf8");
    const b = Buffer.from(partes.v1, "utf8");
    return a.length === b.length && timingSafeEqual(a, b);
  }
  ```

  ```python title="Python" theme={null}
  import hashlib, hmac, time

  def assinatura_valida(corpo_cru: bytes, header: str, segredo: str) -> bool:
      partes = dict(p.split("=", 1) for p in header.split(","))
      try:
          timestamp = int(partes["t"]); recebida = partes["v1"]
      except (KeyError, ValueError):
          return False
      if abs(int(time.time()) - timestamp) > 300:
          return False
      esperada = hmac.new(segredo.encode(), f"{timestamp}.".encode() + corpo_cru, hashlib.sha256).hexdigest()
      return hmac.compare_digest(esperada, recebida)
  ```

  ```php title="PHP" theme={null}
  <?php
  function assinaturaValida(string $corpoCru, string $header, string $segredo): bool
  {
      $partes = [];
      foreach (explode(',', $header) as $p) { [$k, $v] = array_pad(explode('=', $p, 2), 2, null); $partes[$k] = $v; }
      if (empty($partes['t']) || empty($partes['v1'])) return false;
      if (abs(time() - (int) $partes['t']) > 300) return false;
      $esperada = hash_hmac('sha256', $partes['t'] . '.' . $corpoCru, $segredo);
      return hash_equals($esperada, $partes['v1']);
  }
  ```

  ```go title="Go" theme={null}
  func AssinaturaValida(corpoCru []byte, header, segredo string) bool {
  	partes := map[string]string{}
  	for _, p := range strings.Split(header, ",") {
  		if k, v, ok := strings.Cut(p, "="); ok { partes[k] = v }
  	}
  	ts, err := strconv.ParseInt(partes["t"], 10, 64)
  	if err != nil || partes["v1"] == "" { return false }
  	if d := time.Now().Unix() - ts; d > 300 || d < -300 { return false }
  	mac := hmac.New(sha256.New, []byte(segredo))
  	fmt.Fprintf(mac, "%d.", ts); mac.Write(corpoCru)
  	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(partes["v1"]))
  }
  ```
</CodeGroup>

## Pegar o corpo cru

<CodeGroup>
  ```ts title="Express" theme={null}
  app.post("/webhooks/legitimuz", express.raw({ type: "application/json" }), (req, res) => {
    const corpoCru = req.body.toString("utf8");
    if (!assinaturaValida(corpoCru, req.get("X-Legitimuz-Signature") ?? "", process.env.LEGITIMUZ_WEBHOOK_SECRET!)) {
      return res.sendStatus(401);
    }
    res.sendStatus(200);
  });
  ```

  ```ts title="Next.js" theme={null}
  export async function POST(request: Request) {
    const corpoCru = await request.text();
    if (!assinaturaValida(corpoCru, request.headers.get("x-legitimuz-signature") ?? "", process.env.LEGITIMUZ_WEBHOOK_SECRET!)) {
      return new Response(null, { status: 401 });
    }
    return new Response(null, { status: 200 });
  }
  ```
</CodeGroup>

No Express, declare `express.raw()` na própria rota. Um `express.json()` global antes dela consome
o corpo.

## Retentativa e idempotência

Se o seu servidor não responder `2xx`, a entrega é tentada de novo em intervalos crescentes.

| Tentativa | Espera     |
| --------: | ---------- |
|        2ª | 1 minuto   |
|        3ª | 5 minutos  |
|        4ª | 30 minutos |
|        5ª | 2 horas    |
|        6ª | 6 horas    |

Depois disso a entrega é marcada como falha. O reenvio manual fica disponível na aba **Webhooks** da
integração e de cada verificação.

Uma entrega pode chegar duas vezes. Descarte a repetição pelo header `X-Legitimuz-Delivery`:

```ts theme={null}
const inedita = await db.entregas.registrarSeInedita(entregaId); // INSERT com chave única
if (!inedita) return res.sendStatus(200);
```

A ordem de chegada não é garantida. Se a ordem importa, use `occurred_at` do corpo.

## Outros headers

| Header                 | Para quê                                                |
| ---------------------- | ------------------------------------------------------- |
| `X-Legitimuz-Event`    | o nome do evento                                        |
| `X-Legitimuz-Delivery` | o identificador da entrega, a sua chave de idempotência |
| `X-Legitimuz-Attempt`  | o número da tentativa                                   |

## Trocar o segredo

O `signing_secret` sai uma vez só, na criação do endpoint, e não há rotação: nem no dashboard, nem
na API. Trocar o segredo é criar um endpoint novo com a mesma URL e remover o antigo depois.

Enquanto os dois existirem, aceite as duas assinaturas.

```ts title="Aceitar dois segredos durante a troca" theme={null}
const segredos = [process.env.LEGITIMUZ_WEBHOOK_SECRET!, process.env.LEGITIMUZ_WEBHOOK_SECRET_ANTIGO]
  .filter((s): s is string => Boolean(s));

const confere = segredos.some((segredo) => assinaturaValida(corpoCru, header, segredo));
```

<Warning>
  Nessa janela cada evento chega **duas vezes**, uma por endpoint, e as duas entregas têm
  `X-Legitimuz-Delivery` diferente — a chave de idempotência da retentativa não descarta a cópia do
  outro endpoint. Deduplique pelo par verificação + evento enquanto os dois existirem.
</Warning>

Remova `LEGITIMUZ_WEBHOOK_SECRET_ANTIGO` e o endpoint antigo depois que as entregas em voo
chegarem. O passo a passo no dashboard está em
[trocar o segredo de um endpoint](/platform/webhooks#trocar-o-segredo-de-um-endpoint).

## Próximo passo

<Card title="Receptor completo" icon="flask" href="/guides/pocs/webhook-receiver" horizontal>
  Uma POC com conferência, deduplicação e fila.
</Card>
