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

# Webhooks (Saída)

> Receba eventos do SalesOS em tempo real no seu endpoint HTTPS — envelope, verificação de assinatura, catálogo de eventos, retentativas e idempotência.

# Webhooks (Saída)

Webhooks permitem que o SalesOS **chame o seu sistema** quando algo acontece (um lead é ofertado, um
ranking é publicado, uma mensagem é disparada). Você nos dá uma URL HTTPS e um segredo; nós fazemos
`POST` de um payload JSON assinado sempre que um evento assinado dispara.

<Note>
  Esta página cobre webhooks de **saída** (SalesOS → seu endpoint). Para enviar dados **para dentro** do
  SalesOS, veja [Atividades](/pt/api/integrations/activities) e [API Keys](/pt/api/integrations/api-keys).
</Note>

## Configurar um webhook no Dashboard

Vá em **Integrações → Webhooks → Novo Webhook** e preencha o formulário.

<Steps>
  <Step title="Básico">
    * **Nome** (obrigatório) — ex.: `Notificar CRM`.
    * **Chave** — um identificador estável (ex.: `notify_crm`).
    * **Descrição** — opcional.
  </Step>

  <Step title="Eventos (gatilhos)">
    Escolha uma **Categoria** e selecione os **eventos** que disparam este webhook — esta é a sua
    assinatura (veja o [catálogo](#catálogo-de-eventos)). Deixe vazio para um webhook que você só
    dispara manualmente (botão *Testar*) ou via workflow.
  </Step>

  <Step title="Destino">
    * **Método** — `POST` (padrão), `PUT` ou `PATCH`.
    * **Timeout (segundos)** — quanto esperamos pelo seu `2xx` (padrão 30).
    * **URL** (obrigatório) — seu endpoint. **Precisa ser `https://`** e público (hosts internos/loopback são bloqueados).
    * **Autenticação** — escolha **HMAC** para assinar cada entrega e, em **Configuração de Autenticação (JSON)**, informe o segredo:
      ```json theme={null}
      { "secret": "whsec_seu_segredo" }
      ```
      (Outros esquemas disponíveis: Bearer, API Key, Basic, OAuth2.)
  </Step>

  <Step title="Avançado">
    * **Headers Customizados (JSON)** — headers extras enviados em cada entrega.
    * **Template do Payload (JSON com variáveis `{{ }}`)** — molda o `data` do envelope a partir do
      contexto do evento, ex.: `{ "id": "{{event.id}}", "type": "{{event.type}}", "to": "{{event.to}}" }`.

    <Warning>
      Os valores do template são interpolados em JSON. Use campos **planos e escalares**. Injetar um objeto
      aninhado via `"{{event.data}}"` pode não renderizar limpo hoje — confira sempre o **Preview** / envie um **Teste** antes.
    </Warning>
  </Step>

  <Step title="Preview, testar e salvar">
    O formulário mostra um **Preview** ao vivo (headers + corpo). Clique em **Salvar webhook** e use
    **Testar Webhook** para enviar uma entrega de exemplo e confirmar que seu endpoint recebe — e verifica — o payload.
  </Step>
</Steps>

## O envelope

No caminho de evento, cada entrega é um único objeto JSON:

```json theme={null}
{
  "id": "evt_2KWPBgLlAfxdpx2AI54pPJ85f4W",
  "type": "ranking.weekly.published",
  "version": "1",
  "ts": "2026-06-03T12:00:00.000Z",
  "tenant": "9b1f0c2e-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
  "data": { /* específico do evento — veja o catálogo */ }
}
```

| Campo     | Descrição                                                                       |
| --------- | ------------------------------------------------------------------------------- |
| `id`      | Id único do evento, **estável entre retentativas** → sua chave de idempotência. |
| `type`    | Tipo do evento (veja o catálogo).                                               |
| `version` | Versão do schema de `data`. Evolui de forma aditiva.                            |
| `ts`      | Quando o evento ocorreu (ISO 8601, UTC).                                        |
| `tenant`  | Tenant de origem (uuid).                                                        |
| `data`    | Payload específico do evento.                                                   |

## Headers

| Header                  | Descrição                                                    |
| ----------------------- | ------------------------------------------------------------ |
| `X-SalesOS-Event`       | Tipo do evento — roteie sem ler o corpo.                     |
| `X-SalesOS-Event-Id`    | Igual ao `id`; estável entre retentativas (idempotência).    |
| `X-SalesOS-Delivery-Id` | Id da tentativa (muda a cada retry; para debug).             |
| `X-SalesOS-Timestamp`   | Unix em segundos — parte da assinatura (anti-replay).        |
| `X-SalesOS-Tenant`      | Id do tenant de origem.                                      |
| `X-SalesOS-Signature`   | `sha256=<hmac>` — veja [Verificar](#verificar-a-assinatura). |

## Verificar a assinatura

<Warning>
  Sempre verifique a assinatura antes de confiar numa entrega. Sem ela, qualquer um que descobrir sua URL
  poderia forjar eventos.
</Warning>

No caminho de evento, a assinatura cobre **`{id}.{timestamp}.{rawBody}`** (padrão Standard Webhooks),
então autentica o payload **e** o timestamp (anti-replay). Recompute o HMAC com o seu segredo, compare
em tempo constante e rejeite se o timestamp estiver mais de **300s** fora.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  // raw = o corpo cru exato da requisição (NÃO faça JSON.parse antes)
  export function verify(req, secret) {
    const id  = req.headers["x-salesos-event-id"];
    const ts  = req.headers["x-salesos-timestamp"];
    const sig = req.headers["x-salesos-signature"]; // "sha256=<hex>"
    const raw = req.rawBody;

    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // janela de replay

    const expected =
      "sha256=" + crypto.createHmac("sha256", secret).update(`${id}.${ts}.${raw}`).digest("hex");

    const a = Buffer.from(sig), b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

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

  def verify(headers, raw_body: bytes, secret: str) -> bool:
      event_id = headers["X-SalesOS-Event-Id"]
      ts       = headers["X-SalesOS-Timestamp"]
      sig      = headers["X-SalesOS-Signature"]  # "sha256=<hex>"

      if abs(time.time() - int(ts)) > 300:        # janela de replay
          return False

      msg = f"{event_id}.{ts}.{raw_body.decode()}".encode()
      expected = "sha256=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
      return hmac.compare_digest(sig, expected)
  ```
</CodeGroup>

<Note>
  **Entregas de teste** (o botão "Testar Webhook") e gatilhos legados de workflow usam hoje um esquema mais
  simples: o corpo é o template renderizado cru (não o envelope) e o header de assinatura é
  `X-Signature: <prefixo><hmac(corpo)>` — **sem** o prefixo `id.timestamp.`, então sem anti-replay.
  Prefira o caminho de evento acima em produção.
</Note>

## Catálogo de eventos

| `type`                           | Quando dispara                        | `data`                         |
| -------------------------------- | ------------------------------------- | ------------------------------ |
| `lead.offer_created`             | Um lead é ofertado a um corretor      | payload do lead/oportunidade   |
| `lead.accepted`                  | Um corretor aceita um lead            | payload do lead/oportunidade   |
| `lead.offer_expired`             | Uma oferta expira sem aceite          | payload do lead/oportunidade   |
| `mission.completed`              | Uma missão é concluída                | payload da missão              |
| `ranking.weekly.published`       | Ranking semanal publicado (agendado)  | leaderboard (linhas por email) |
| `ranking.consolidated.published` | Ranking consolidado publicado         | leaderboard (linhas por email) |
| `message.dispatch`               | Mensagem disparada a um usuário/grupo | `{ to, m }`                    |
| `notification.push.dispatch`     | Push disparado a um usuário/grupo     | `{ to, n }`                    |

Exemplo de `data` para `ranking.weekly.published`:

```json theme={null}
{
  "p": { "type": "weekly", "label": "Semana 23/2026", "start": "2026-05-25", "end": "2026-05-31" },
  "org_unit": null,
  "totals": { "users": 843, "points": 1820400, "missions": 5120 },
  "rows": [
    { "rank": 1, "delta": 2, "email": "maria@loja.com", "points": 4820, "missions": 37, "org_unit": "ou_12" }
  ]
}
```

Os destinatários são chaveados por **email** — nomes e ids não são enviados; enriqueça do seu lado se precisar.

## Confiabilidade

**Responda rápido.** Retorne um status `2xx` rapidamente (antes de processamento pesado) para confirmar o recebimento.

| Sua resposta                      | O que o SalesOS faz                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------------------ |
| `2xx`                             | Marca a entrega como concluída. Sem retry.                                                       |
| `4xx` (400, 401, 422…)            | Tratado como rejeição permanente. **Sem retry** (exceto `429`, que respeita o backoff).          |
| `5xx` / timeout / erro de conexão | Reentrega com **backoff exponencial** (`2^n` minutos, até 5 tentativas), depois **dead-letter**. |

**Idempotência.** O mesmo evento pode ser entregue mais de uma vez (retentativas). Deduplique por
`X-SalesOS-Event-Id` — ele permanece o mesmo entre retentativas.

<Tip>
  Entregas falhas aparecem no painel **Entregas** (Dashboard → Integrações → Webhooks), onde você inspeciona
  status, tentativas e o último erro, e pode reenviar a partir da fila de dead-letter.
</Tip>
