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

# Webhooks

> Envie cada alerta de receita para o seu próprio sistema como uma requisição HTTPS assinada, e confira que ela veio da Stealth.

Um webhook envia cada alerta de receita para outro sistema, como a sua ferramenta de chamados, um bot de chat ou um painel interno, numa requisição HTTPS `POST` assinada pela Stealth. Proprietários e Administradores gerenciam os webhooks em **Integrações > Notificações**. Uma organização pode ter até 10.

## Como funciona

* Depois de cada verificação, cada evento de incidente que passa pelas [regras da organização](https://docs.usestealth.dev/pt-BR/alerts/overview.md) (eventos, gravidade mínima e exceções) e pelo filtro do próprio webhook (gravidade mínima e eventos) é enviado ao webhook.
* Os eventos são `opened` (**Nova queda**), `escalated` (**Queda piorou**) e `resolved` (**Normalizou**). Uma piora vinda de abaixo da gravidade mínima do webhook chega como `opened`, porque é a primeira vez que o webhook fica sabendo do incidente.
* Cada evento é uma requisição, e as requisições para um mesmo webhook saem em ordem. Os webhooks nunca esperam o resumo diário.
* Os eventos por hora vêm da verificação regular de cada hora. Uma verificação pedida com **Verificar agora** pode enviar eventos das regras diárias.

## Passo a passo

**1. Prepare o endpoint**

O endpoint precisa usar `https://` na porta padrão (443) com certificado válido, ter um nome de host público e responder com `2xx` em até 5 segundos. Endereços IP, `localhost`, nomes sem ponto e sufixos internos como `.local` e `.internal` são recusados. Usuário ou senha na URL são recusados. Um token no caminho ou na query é aceito.

**2. Adicione o webhook**

Vá em **Integrações > Notificações** e escolha **Adicionar webhook**. Preencha **Nome** e **URL do endpoint**, escolha a **Gravidade mínima** (**Só Crítico**, **Crítico e Atenção** ou **Todas as gravidades**) e os **Eventos** (**Nova queda**, **Queda piorou**, **Normalizou**), e escolha **Salvar**.

**3. Copie o segredo de assinatura**

A Stealth mostra o **Segredo de assinatura** (ele começa com `whsec_`) uma única vez. Copie para o seu gerenciador de segredos. Para trocá-lo depois, remova o webhook e adicione de novo.

**4. Envie um teste**

Escolha **Enviar teste**. A Stealth envia o exemplo abaixo com `"test": true` e a sua própria organização, numa única tentativa, sem novas tentativas. O `incident.url` aponta para um incidente que não existe. O teste também funciona num webhook pausado, e dá para enviar um a cada 10 segundos. O resultado aparece numa mensagem e em **Envios**.

**5. Confira a assinatura no seu código**

Recuse toda requisição cuja assinatura não confere, como mostra [Conferir a assinatura](#conferir-a-assinatura).

### Requisição

```http theme={null}
POST /seu/caminho HTTP/1.1
Content-Type: application/json
User-Agent: Stealth-Webhooks/1
Stealth-Signature: t=1791554400,v1=<64 caracteres hexadecimais>
```

### Corpo (versão v1)

```json payload.json theme={null}
{
  "version": "v1",
  "id": "evt_4f1c2b7e9a0d4e6b8c3a5f7d9e1b2c4a",
  "test": false,
  "event": "opened",
  "severity": "critical",
  "kind": "unit_silent",
  "occurred_at": "2026-10-09T14:00:00Z",
  "organization": {
    "id": "org_882ae4d9",
    "name": "Acme Media"
  },
  "network": {
    "code": "12345678",
    "name": "Acme Network"
  },
  "client": {
    "code": "987654",
    "name": "Example News"
  },
  "ad_unit": {
    "id": "23145678901",
    "name": "EXAMPLE_TOP",
    "format": "top"
  },
  "incident": {
    "id": "7d3f9a52-1c4e-4b8a-9f61-2e5d8c0b4a17",
    "started_at": "2026-10-09T13:00:00Z",
    "url": "https://app.usestealth.dev/console/incidents/7d3f9a52-1c4e-4b8a-9f61-2e5d8c0b4a17"
  },
  "estimated_loss": {
    "per_hour": 35.2,
    "currency": "BRL"
  }
}
```

| Campo | Tipo | Descrição |
| - | - | - |
| `version` | string | Sempre `v1` neste formato. |
| `id` | string | Id único do evento: `evt_` e 32 caracteres hexadecimais. Ele se repete em todas as tentativas e em todos os webhooks da organização. Use-o para descartar duplicados. |
| `test` | boolean | `true` só para eventos enviados por **Enviar teste**. |
| `event` | string | `opened`, `escalated` ou `resolved`. |
| `severity` | string | `critical`, `attention` ou `light`. Em `resolved`, a gravidade que o incidente tinha. |
| `kind` | string | `unit_silent`, `no_fill`, `client_traffic_drop`, `client_fill_drop`, `yield_drop` ou `trend_down`. Veja [Tipos de alerta](https://docs.usestealth.dev/pt-BR/detection/alert-kinds.md). |
| `occurred_at` | string | Quando a Stealth detectou o evento, ISO 8601 em UTC. |
| `organization` | objeto | O `id` curto da organização e o `name`. |
| `network` | objeto | O `code` e o `name` da rede no Google Ad Manager. |
| `client` | objeto ou null | O `code` e o `name` do cliente (publisher filho). `null` quando o incidente não tem um cliente próprio, como numa rede sem parceiros. |
| `ad_unit` | objeto ou null | O `id` do bloco no Google Ad Manager, o `name` e o `format` (`anchor`, `intext`, `infeed`, `vignette`, `endOfArticle`, `top`, `side`, `footer`, `flex`, `video` ou `other`). `null` num incidente do cliente inteiro. |
| `incident` | objeto | O `id` do incidente na Stealth, `started_at` (ISO 8601, UTC) e `url`, que abre o cliente do incidente no painel. |
| `estimated_loss` | objeto | `per_hour`: a perda estimada por hora em unidades da moeda, com duas casas. Em `resolved`, a média por hora do incidente inteiro. `currency`: o código ISO 4217 da moeda da rede. |

Campos novos podem entrar na `v1` a qualquer momento, então ignore os campos que você não conhece. Renomear ou remover um campo exige uma `v2`, anunciada no [histórico de mudanças](https://docs.usestealth.dev/pt-BR/changelog.md).

### Conferir a assinatura

`Stealth-Signature` é `t=<segundos unix>,v1=<hex>`, em que o hex é o HMAC-SHA256 de `<t>.<corpo bruto>`, com o segredo de assinatura do webhook como chave (a string inteira, incluindo `whsec_`).

1. Leia o corpo bruto antes de interpretar o JSON.
2. Calcule o HMAC e compare em tempo constante.
3. Recuse um `t` com mais de 5 minutos de diferença do seu relógio, para bloquear repetições.

```js verify.js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyStealthSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const given = Buffer.from(parts.v1 ?? '', 'hex')
  return given.length === expected.length && timingSafeEqual(given, expected)
}
```

```python verify.py theme={null}
import hashlib, hmac, time

def verify_stealth_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t = parts.get("t", "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

### Respostas, tentativas e pausa

* Responda com qualquer `2xx` em até 5 segundos. Se o seu processamento demora mais, responda primeiro e processe depois.
* A Stealth tenta até 3 vezes, esperando 1 segundo e depois 3 segundos, após um timeout, um erro de rede, `408`, `429` ou um `5xx`. Qualquer outra resposta falha na hora. Redirecionamentos nunca são seguidos, e o corpo da resposta nunca é lido.
* Quando um envio falha em todas as tentativas, os outros eventos daquela verificação não são enviados a esse webhook e aparecem como **Não enviado** no histórico. Eventos que falharam não são reenviados depois.
* Cada webhook tem até 60 segundos de cada verificação. Quando esse tempo acaba, os envios que ainda esperavam aparecem como **Não enviado**, com uma nota de que o tempo acabou. Eles não contam como falha e não são reenviados.
* Um webhook é pausado quando está falhando há 24 horas e pelo menos 3 verificações falharam nesse período. Todos os membros da organização recebem um aviso no app e por e-mail, e Proprietários e Administradores recebem o link para retomar. Um envio que funciona, ou um **Enviar teste** que recebe `2xx`, zera a contagem. Os eventos que acontecem enquanto ele está pausado não são guardados. Escolha **Retomar** quando o endpoint voltar a funcionar.

## O que você vê na tela

Cada webhook em **Integrações > Notificações** mostra a situação (**Ativo** ou **Pausado**) e, quando está falhando, desde quando. **Envios** mostra os últimos 20 envios: **Entregue**, **Falhou** ou **Não enviado**, com o status HTTP, as tentativas e a duração. A Stealth guarda o resultado, nunca o conteúdo. Uma verificação acrescenta no máximo 10 linhas **Não enviado** por webhook.

O menu da linha tem **Editar**, **Pausar** ou **Retomar**, e **Remover**. Remover um webhook o interrompe na hora e apaga o histórico de envios.

### Como a Stealth protege as requisições que envia

Um webhook faz os servidores da Stealth enviarem uma requisição para um endereço que você escolhe, por isso a Stealth limita para onde essa requisição pode ir:

* Só `https`, só a porta 443, e o certificado precisa ser válido.
* Nada de endereços IP nem de nomes privados ou internos.
* Redirecionamentos nunca são seguidos, e o corpo da resposta nunca é lido.
* O endereço é conferido quando você salva e de novo antes de cada envio.

A Stealth não confere para qual endereço IP o nome do host aponta no momento do envio, e uma conferência assim poderia ser burlada trocando o DNS do nome entre a conferência e a requisição. Por isso, um nome público apontado para um endereço privado não é barrado. Use só endpoints que você controla e cujo DNS é de confiança. **Enviar teste** também sai dos servidores da Stealth, não do seu navegador, então o endpoint precisa estar acessível pela internet.

## Perguntas frequentes

> [accordion] Como evito processar o mesmo evento duas vezes?
>
> Use o campo `id`. Ele se repete em todas as tentativas e em todos os webhooks da organização.

> [accordion] Posso mandar os alertas para o Slack, o Microsoft Teams ou o Discord?
>
> Não diretamente. Aponte o webhook para um pequeno serviço ou uma ferramenta de automação que receba a requisição, confira a assinatura e publique a mensagem no seu chat.

> [accordion] O webhook traz a leitura da origem da demanda?
>
> Não. A versão `v1` traz os campos listados acima. A leitura aparece no painel e no e-mail de abertura dos incidentes diários.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.