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

# Assinatura das notificações

> Como confirmar que uma notificação de webhook foi enviada pela Spedy.

Toda notificação enviada pela Spedy é assinada. A assinatura permite confirmar
que o POST recebido partiu da Spedy e que o corpo não foi alterado no caminho —
sem ela, qualquer pessoa que descubra a URL do seu endpoint consegue enviar um
evento forjado.

A assinatura segue o padrão aberto [Standard
Webhooks](https://standardwebhooks.com), o que permite usar bibliotecas prontas
em vez de implementar a verificação do zero.

<Note>
  Verificar a assinatura é **opcional**. O corpo das notificações não mudou e
  integrações criadas antes deste recurso continuam funcionando sem alteração.
</Note>

## Os headers da assinatura

Cada entrega inclui três headers:

| Header              | Conteúdo                                                                                  |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `webhook-id`        | Identificador da entrega. É o mesmo valor do campo `id` do corpo — use-o para deduplicar. |
| `webhook-timestamp` | Momento do envio, em segundos desde a época Unix (UTC).                                   |
| `webhook-signature` | Uma ou mais assinaturas, no formato `v1,<base64>`, separadas por espaço.                  |

```http theme={null}
POST /webhooks/spedy HTTP/1.1
Content-Type: application/json; charset=utf-8
webhook-id: 3f2b1c9e-5a7d-4e18-9c3a-7b6f2d1e8a04
webhook-timestamp: 1754932800
webhook-signature: v1,K5oT9r8GKYqrTwjUPD8ILPZIo2LaLaSwMfKQ9r8=
```

O header pode trazer **mais de uma assinatura** durante uma janela de rotação
(veja [Rotacionar o segredo](#rotacionar-o-segredo)). Considere a entrega válida
quando **qualquer uma** delas conferir.

## Obter o segredo da conta

O segredo é **da conta** — o mesmo valor assina as entregas de todos os webhooks
configurados, de todas as empresas.

```bash cURL theme={null}
curl https://api.spedy.com.br/v1/webhooks/secret \
  -H "X-Api-Key: sua-chave-de-api"
```

```json theme={null}
{
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw",
  "createdAt": "2026-08-21T15:11:50+00:00",
  "previousSecretExpiresAt": null
}
```

| Campo                     | Descrição                                                                           |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `secret`                  | O segredo em claro, no formato `whsec_` + base64. Guarde-o como credencial.         |
| `createdAt`               | Quando o segredo atual passou a valer.                                              |
| `previousSecretExpiresAt` | Até quando o segredo anterior ainda é aceito. `null` fora de uma janela de rotação. |

O segredo pode ser consultado quantas vezes for necessário — não é revelado uma
única vez. Se você o perdeu, consulte-o novamente; não é preciso rotacionar.

<Warning>
  `createdAt` e `previousSecretExpiresAt` são devolvidos em **UTC**, com offset
  explícito (`+00:00`). Os demais campos de data da API usam o horário de São
  Paulo. Considere o offset ao calcular o fim de uma janela de rotação.
</Warning>

## Como verificar

O conteúdo assinado é a concatenação do identificador da entrega, do timestamp e
do **corpo exatamente como recebido**, separados por ponto:

```
{webhook-id}.{webhook-timestamp}.{corpo}
```

Sobre esse conteúdo aplica-se HMAC com SHA-256, usando como chave os bytes
resultantes da decodificação em base64 do segredo **sem o prefixo `whsec_`**. O
resultado é codificado em base64 e comparado com cada entrada `v1,` do header.

<Warning>
  Use o corpo **cru** da requisição, byte a byte. Desserializar o JSON e
  serializá-lo de novo altera espaçamento e ordem de campos, e a assinatura
  deixa de conferir. Na maioria dos frameworks isso significa ler o corpo antes
  de qualquer middleware de parsing.
</Warning>

### Com uma biblioteca

O site do [Standard Webhooks](https://standardwebhooks.com) mantém bibliotecas
oficiais para várias linguagens. Elas recebem o segredo, os headers e o corpo
cru, e cuidam da verificação e da tolerância de tempo.

### Sem biblioteca

<CodeGroup>
  ```js Node.js theme={null}
  const crypto = require("crypto");

  const TOLERANCIA_SEGUNDOS = 300;

  function verificarAssinatura(secret, headers, corpoCru) {
    const id = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];

    const agora = Math.floor(Date.now() / 1000);
    if (Math.abs(agora - Number(timestamp)) > TOLERANCIA_SEGUNDOS) {
      return false;
    }

    const chave = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const conteudo = `${id}.${timestamp}.${corpoCru}`;
    const esperada = crypto.createHmac("sha256", chave).update(conteudo).digest("base64");

    return headers["webhook-signature"].split(" ").some((parte) => {
      const [versao, assinatura] = parte.split(",");
      if (versao !== "v1" || !assinatura) {
        return false;
      }
      const a = Buffer.from(assinatura);
      const b = Buffer.from(esperada);
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    });
  }
  ```

  ```python Python theme={null}
  import base64
  import hashlib
  import hmac
  import time

  TOLERANCIA_SEGUNDOS = 300


  def verificar_assinatura(secret: str, headers: dict, corpo_cru: bytes) -> bool:
      webhook_id = headers["webhook-id"]
      timestamp = headers["webhook-timestamp"]

      if abs(int(time.time()) - int(timestamp)) > TOLERANCIA_SEGUNDOS:
          return False

      chave = base64.b64decode(secret.removeprefix("whsec_"))
      conteudo = f"{webhook_id}.{timestamp}.".encode() + corpo_cru
      esperada = base64.b64encode(hmac.new(chave, conteudo, hashlib.sha256).digest()).decode()

      for parte in headers["webhook-signature"].split(" "):
          versao, _, assinatura = parte.partition(",")
          if versao == "v1" and hmac.compare_digest(esperada, assinatura):
              return True

      return False
  ```

  ```php PHP theme={null}
  <?php

  const TOLERANCIA_SEGUNDOS = 300;

  function verificarAssinatura(string $secret, array $headers, string $corpoCru): bool
  {
      $id = $headers['webhook-id'];
      $timestamp = $headers['webhook-timestamp'];

      if (abs(time() - (int) $timestamp) > TOLERANCIA_SEGUNDOS) {
          return false;
      }

      $chave = base64_decode(substr($secret, strlen('whsec_')));
      $conteudo = "{$id}.{$timestamp}.{$corpoCru}";
      $esperada = base64_encode(hash_hmac('sha256', $conteudo, $chave, true));

      foreach (explode(' ', $headers['webhook-signature']) as $parte) {
          [$versao, $assinatura] = array_pad(explode(',', $parte, 2), 2, '');

          if ($versao === 'v1' && hash_equals($esperada, $assinatura)) {
              return true;
          }
      }

      return false;
  }
  ```
</CodeGroup>

<Warning>
  Compare as assinaturas com uma função de tempo constante — `timingSafeEqual`
  em Node.js, `hmac.compare_digest` em Python, `hash_equals` em PHP. Comparação
  com `==` ou `!=` vaza informação pelo tempo de execução.
</Warning>

## Tolerância de tempo e retentativas

O `webhook-timestamp` permite rejeitar entregas antigas reenviadas por
terceiros. A tolerância recomendada é de **5 minutos**.

Uma entrega que falha é reenviada com [backoff
crescente](/webhooks/gerenciamento#política-de-retry), e **cada retentativa é
assinada de novo**, com timestamp do momento do reenvio. Uma retentativa que
chega 16 horas depois passa normalmente na tolerância de 5 minutos. O
`webhook-id`, por outro lado, permanece o mesmo em todas as tentativas — é por
ele que se deduplica.

## Rotacionar o segredo

Rotacione o segredo quando suspeitar de exposição ou por política interna. A
troca gera um segredo novo e mantém o anterior válido por um período de
transição:

```bash cURL theme={null}
curl -X POST https://api.spedy.com.br/v1/webhooks/secret/rotate \
  -H "X-Api-Key: sua-chave-de-api" \
  -H "Content-Type: application/json" \
  -d '{ "overlapHours": 24 }'
```

```json theme={null}
{
  "secret": "whsec_novoSegredoEmBase64AquiXYZ123",
  "previousSecretExpiresAt": "2026-08-22T15:11:50+00:00"
}
```

| Campo          | Descrição                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `overlapHours` | Horas em que o segredo anterior continua sendo aceito. Aceita de `0` a `72`; quando omitido, aplica `24`. O corpo inteiro é opcional. |

Durante a janela, **cada entrega vai assinada com os dois segredos** — o novo e
o anterior — no mesmo header `webhook-signature`. Isso permite atualizar o
segredo no seu sistema quando for conveniente, sem perder entregas. Encerrada a
janela, apenas o segredo novo é usado.

`overlapHours: 0` invalida o segredo anterior imediatamente. Use essa opção
quando o segredo tiver sido exposto e a interrupção de entregas for aceitável
até você atualizar o seu lado.

<Note>
  Uma nova rotação dentro de uma janela ainda aberta descarta o segredo mais
  antigo: a cada momento valem no máximo dois segredos.
</Note>

<CardGroup cols={2}>
  <Card title="Gerenciamento de webhooks" icon="gear" href="/webhooks/gerenciamento">
    Crie, edite, desabilite e reative webhooks via API. Política de retry.
  </Card>

  <Card title="Eventos e payload" icon="list-check" href="/webhooks/eventos-e-payload">
    Tipos de evento disponíveis e o formato completo do payload.
  </Card>
</CardGroup>
