# Webhooks

Um webhook é a forma como [Eventos](/pt-br/products/events/) entrega a um servidor que você opera: todo evento que corresponde a uma assinatura chega ao seu endpoint HTTPS como um `POST` assinado. Este guia cobre a criação de uma assinatura de webhook, a requisição que o seu endpoint recebe, a verificação da assinatura, como funcionam as novas tentativas e as mensagens mortas, e a rotação do segredo de assinatura sem perder nenhuma entrega.

## O que é uma assinatura de webhook

Uma assinatura diz quais eventos um receptor quer e como eles chegam até ele. Uma **assinatura de webhook** tem uma lista de padrões de tipo e um bloco de entrega com `"mode": "webhook"` e a sua `url`. A partir daí, cada evento publicado no espaço de trabalho cujo `type` corresponda a um dos padrões é enviado por POST a essa URL, uma vez por assinatura.

A entrega é **de pelo menos uma vez**. Em operação normal cada evento chega uma vez; uma nova tentativa, uma resposta lenta ou uma conexão perdida pode fazê-lo chegar de novo. O seu receptor deve, portanto, ser construído para aceitar uma duplicata sem consequências, usando o id do evento como chave.

A URL deve ser `https`, na porta 443, sem nome de usuário ou senha, sem fragmento e com um host público: nomes privados, de loopback, de link local e de uso local são recusados quando a assinatura é criada e verificados de novo a cada entrega. Redirecionamentos nunca são seguidos.

## Crie uma

Chame `POST https://events.inovacc.dev/v1/workspaces/{workspace}/subscriptions` com uma chave secreta que tenha a permissão de escrita de assinaturas:

```json
{"types": ["order.*"], "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc"}}
```

A resposta é `201`:

```json
{"subscription_id": "sub_...", "types": ["order.*"],
 "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc", "secret_ref": "managed"},
 "created_at": "2026-10-06T12:00:00.000Z",
 "signing_secret": "whsec_..."}
```

`signing_secret` é a chave que o seu endpoint usa para verificar as entregas. **Ele é mostrado uma única vez, nesta resposta, e nunca mais**: as listagens mostram `"secret_ref": "managed"` e nada mais. Guarde-o imediatamente no seu gerenciador de segredos. Se for perdido, rotacione-o (abaixo). Um espaço de trabalho comporta no máximo 50 assinaturas, cada uma com 1 a 20 padrões.

## A requisição de entrega

Cada entrega é:

```http
POST https://hooks.example.com/inovacc
content-type: application/json
x-event-id: evt_01K...
x-event-type: order.paid
x-event-timestamp: 1791201600
x-event-signature: v1=<64 lowercase hex characters>

{"event_id":"evt_01K...","type":"order.paid", ... }
```

`x-event-timestamp` é o horário Unix, em segundos, em que a entrega foi assinada. Durante uma rotação de segredo, `x-event-signature` traz uma entrada por segredo ativo, a mais nova primeiro, separadas por vírgulas: `v1=<new>,v1=<old>`.

Responda com qualquer status `2xx` para confirmar a entrega. Qualquer outra coisa (um `4xx`, um `5xx`, nenhuma resposta em 10 segundos, um erro de rede) é uma tentativa que falhou e será repetida.

## O envelope do evento

O corpo é o envelope do evento, exatamente como os assinantes de todos os modos o recebem:

| Campo | Significado |
|---|---|
| `event_id` | `evt_` seguido de 26 caracteres, atribuído pela Inovacc quando o evento foi publicado. Único por evento. |
| `type` | O tipo do evento, como `order.paid`: palavras minúsculas separadas por pontos, até 128 bytes. |
| `source` | A aplicação que publicou o evento, obtida da sua credencial. |
| `time` | Quando foi publicado, RFC 3339 UTC com milissegundos. |
| `tenant` | A organização, a conta, o espaço de trabalho e, quando conhecida, a aplicação a que o evento pertence. |
| `idempotency_key` | A chave do publicador; quando ele não enviou nenhuma, o `event_id`. |
| `data` | A carga JSON do publicador: o que aconteceu, normalmente ids em vez de objetos inteiros. |

Leia apenas os campos de que precisa e ignore os campos que você não conhece.

## Verifique a assinatura

Verifique cada entrega **antes** de interpretá-la ou agir sobre ela:

1. Leia `x-event-timestamp` e os **bytes brutos do corpo**, exatamente como recebidos, antes de qualquer interpretação de JSON.
2. Calcule `HMAC-SHA256(secret, timestamp + "." + body)`, em que `secret` é a string `whsec_...` inteira em UTF-8, e escreva o resultado em hexadecimal minúsculo.
3. Divida `x-event-signature` pelas vírgulas e aceite a entrega quando **qualquer** entrada `v1=` for igual ao seu valor, comparando em tempo constante.
4. Rejeite um carimbo de data e hora a mais de cinco minutos do seu relógio, para que uma entrega antiga não possa ser reenviada contra você.

**Exemplo prático.** Com o segredo `example-key-0001`, o carimbo `1791201600` e o corpo `{"hello":"world"}`, o texto assinado é `1791201600.{"hello":"world"}` e a assinatura são os 64 caracteres hexadecimais abaixo, impressos aqui em duas metades que você junta sem espaço:

```text
05080ab29a6abfb44033966c7837c2a1
889fbf1a909f9cbd12b9dd5d12bb2bf5
```

de modo que o cabeçalho fica `v1=` seguido das duas metades. Execute o seu verificador neste exemplo antes de implantá-lo.

**TypeScript** (Node.js 18 ou posterior):

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyInovaccWebhook(
  secret: string,
  timestamp: string,
  signatureHeader: string,
  rawBody: Buffer,
  nowSeconds: number = Math.floor(Date.now() / 1000),
): boolean {
  const ts = Number(timestamp);
  if (!Number.isInteger(ts) || Math.abs(nowSeconds - ts) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  return signatureHeader.split(",").some((entry) => {
    const part = entry.trim();
    if (!part.startsWith("v1=")) return false;
    const given = Buffer.from(part.slice(3), "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}
```

**Python** (3.10 ou posterior):

```python
import hashlib
import hmac
import time


def verify_inovacc_webhook(secret: str, timestamp: str, signature_header: str,
                           raw_body: bytes, now: int | None = None) -> bool:
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    now = int(time.time()) if now is None else now
    if abs(now - ts) > 300:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
                        hashlib.sha256).hexdigest()
    return any(
        part.strip().startswith("v1=")
        and hmac.compare_digest(part.strip()[3:], expected)
        for part in signature_header.split(",")
    )
```

Ambas as funções recebem o corpo bruto: configure o seu framework web para entregar os bytes, e não um objeto interpretado, nesta rota.

## Novas tentativas e tempos limite

O seu endpoint tem **10 segundos** para responder. Uma tentativa que falhou é repetida após 10 segundos, e o atraso dobra a cada nova falha até 10 minutos. Quando o limite de 5 tentativas é atingido, o evento vai para as mensagens mortas do seu espaço de trabalho. Uma falha afeta apenas a entrega que falhou: os outros eventos, e as outras assinaturas do mesmo evento, continuam fluindo.

Como uma entrega pode ser repetida, um `2xx` que chega depois de o seu endpoint já ter processado o evento é inofensivo, desde que você deduplique por `x-event-id`.

## Rotacione o segredo

`POST /v1/workspaces/{workspace}/subscriptions/{id}/rotate-secret` devolve um novo segredo, uma única vez:

```json
{"subscription_id": "sub_...", "signing_secret": "whsec_...", "previous_valid_until": "2026-10-07T12:00:00.000Z"}
```

Até `previous_valid_until` (24 horas por padrão), **ambos os segredos assinam**: cada entrega traz `v1=<new>,v1=<old>`. Implante o novo segredo no seu receptor dentro dessa janela; como o verificador aceita qualquer entrada correspondente, nada é perdido durante a troca. Rotacionar de novo dentro da janela mantém todos os segredos não expirados. A rotação se aplica apenas a assinaturas de webhook (`409 wrong_delivery_mode` caso contrário).

## Mensagens mortas e reenvio

`GET /v1/workspaces/{workspace}/dead?limit=50` lista as mensagens mortas, da mais nova para a mais antiga, com o evento, `attempts`, `dead_at`, `replay_count` e `last_error`:

| `last_error` | Significado |
|---|---|
| `webhook_status_<code>` | O seu endpoint respondeu esse status, por exemplo `webhook_status_500`. |
| `webhook_timeout` | Nenhuma resposta em 10 segundos. |
| `webhook_network` | A conexão falhou. |
| `url_destination_blocked` | A URL agora aponta para um lugar aonde as entregas não podem ir. |
| `secret_missing` | O segredo de assinatura da assinatura não pôde ser usado. |

Corrija a causa e depois faça `POST /v1/workspaces/{workspace}/dead/{event_id}/replay`. A resposta é `202`, e o mesmo envelope, com o mesmo `event_id`, é entregue de novo às assinaturas que nunca o receberam. As mensagens mortas são mantidas por 30 dias.

## Boas práticas

- **Verifique antes de interpretar**: calcule a assinatura sobre os bytes brutos e só depois interprete.
- **Torne o receptor idempotente**: guarde cada `x-event-id` processado e responda `2xx` imediatamente para um que você já viu.
- **Responda rápido**: confirme com um `2xx` e faça o trabalho lento depois, a partir de uma tarefa sua.
- **Rejeite carimbos de data e hora obsoletos** (a mais de cinco minutos de diferença) e mantenha o relógio do seu servidor sincronizado.
- **Mantenha o segredo em um gerenciador de segredos** e rotacione-o quando alguém que o conhecia sair, ou em um calendário.
- **Monitore as mensagens mortas** e `dlq_inflow` em `GET /stats`; reenvie quando o endpoint estiver saudável.
- **Devolva `2xx` apenas quando tiver o evento em segurança**: um `2xx` encerra as novas tentativas.

## Veja também

- [Eventos](/pt-br/products/events/): publicação, assinaturas, pull e transmissão
- [Erros](/pt-br/guides/errors/), [Autenticação](/pt-br/guides/authentication/), [Limites](/pt-br/guides/limits/)
