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

# Webhooks

> Receba eventos em tempo real via HTTP POST assinado quando os dados mudam.

# Webhooks

Em vez de consultar a API repetidamente (*polling*), você pode registrar uma **URL** e receber uma requisição `POST` sempre que um evento relevante ocorrer — início de jogo, gol, encerramento, escalação, e mais.

Cada entrega é **assinada** (HMAC-SHA256) para que você confirme que a requisição veio da Dados Futebol e não foi adulterada.

<Info>
  Webhooks estão disponíveis em todos os planos, com cota de **endpoints ativos** e de **entregas mensais** por plano. Configure-os no painel em `https://dadosfutebol.com.br/webhooks` ou via API.
</Info>

## Catálogo de eventos

| Evento                        | Quando dispara                          |
| ----------------------------- | --------------------------------------- |
| `partida.criada`              | Uma nova partida é cadastrada           |
| `partida.ao_vivo`             | A partida entra em andamento            |
| `partida.placar_atualizado`   | O placar muda                           |
| `partida.gol`                 | Um gol é registrado, com autor e minuto |
| `partida.encerrada`           | A partida é encerrada                   |
| `partida.escalacao_publicada` | A escalação é confirmada                |
| `rodada.encerrada`            | Uma rodada é concluída                  |
| `fase.finalizada`             | Uma fase de mata-mata é concluída       |

Ao criar um webhook você escolhe quais eventos quer receber. O plano free recebe eventos apenas dos campeonatos aos quais tem acesso.

<Info>
  `partida.placar_atualizado` e `partida.gol` são complementares: o primeiro traz o placar novo, o segundo diz **quem** marcou e **quando**. Um gol dispara os dois.
</Info>

### `partida.gol`

Além dos campos da partida, o payload traz o objeto `gol` com o lance que acabou de acontecer:

```json theme={null}
{
  "evento": "partida.gol",
  "gerado_em": "2026-07-25T21:52:09-03:00",
  "dados": {
    "id": 2903,
    "placar_mandante": 1,
    "placar_visitante": 1,
    "gol": {
      "tipo": "gol",
      "jogador": "Vegetti",
      "time_nome": "Vasco",
      "minuto": 80
    },
    "gols": [
      { "tipo": "gol", "jogador": "Pedro", "time_nome": "Flamengo", "minuto": 22 },
      { "tipo": "gol", "jogador": "Vegetti", "time_nome": "Vasco", "minuto": 80 }
    ]
  }
}
```

`tipo` é `gol`, `gol_contra` ou `penalti`. `jogador` pode vir `null` quando a fonte ainda não confirmou a autoria — o placar e o minuto seguem válidos.

<Note>
  Cartões e substituições não vão no webhook: para eles use `GET /v1/partidas/{id}/eventos`, que durante o jogo traz todos os lances.
</Note>

## Formato do payload

O corpo é sempre um JSON com este envelope:

```json theme={null}
{
  "evento": "partida.encerrada",
  "gerado_em": "2026-05-27T20:15:00-03:00",
  "dados": {
    "id": 1234,
    "campeonato_id": 4,
    "rodada_numero": 12,
    "fase": null,
    "status": "encerrado",
    "placar_mandante": 2,
    "placar_visitante": 1,
    "time_mandante": { "id": 1, "nome": "Flamengo", "sigla": "FLA" },
    "time_visitante": { "id": 2, "nome": "Palmeiras", "sigla": "PAL" },
    "estadio": "Maracanã",
    "gols": [
      { "tipo": "gol", "jogador": "Pedro", "time_nome": "Flamengo", "minuto": 22 },
      { "tipo": "penalti", "jogador": "Arrascaeta", "time_nome": "Flamengo", "minuto": 61 },
      { "tipo": "gol", "jogador": "Estêvão", "time_nome": "Palmeiras", "minuto": 78 }
    ]
  }
}
```

O objeto em `dados` espelha o recurso correspondente da API (partida, rodada ou fase), acrescido de `gols` nos eventos de partida. Use o `id`/`campeonato_id` para buscar detalhes completos nos endpoints `GET /v1/...` quando precisar.

<Note>
  Em jogos de mata-mata os campos `rodada_*` vêm nulos e `fase` traz `id`, `nome` e `slug` — por exemplo `"Oitavas de Final"`. Em pontos corridos é o contrário. `campeonato_id` está sempre preenchido, venha o vínculo da rodada ou da fase.
</Note>

### `gols`

Todos os eventos `partida.*` carregam a lista de gols da partida até aquele momento, ordenada por minuto — assim você monta a narração do jogo sem uma chamada extra à API. Cada item tem `tipo` (`gol`, `gol_contra` ou `penalti`), `jogador`, `time_nome` e `minuto`.

A chave está **sempre presente**: vem `[]` num 0 a 0 e também quando a fonte ainda não publicou os lances daquele jogo. O placar em `placar_mandante`/`placar_visitante` é quem desempata os dois casos.

### Headers da requisição

| Header            | Descrição                                   |
| ----------------- | ------------------------------------------- |
| `X-DF-Evento`     | Nome do evento (ex.: `partida.encerrada`)   |
| `X-DF-Entrega`    | ID único da entrega — use para idempotência |
| `X-DF-Timestamp`  | Unix timestamp usado na assinatura          |
| `X-DF-Assinatura` | `sha256=<hmac>` do corpo (ver abaixo)       |

## Verificação da assinatura

Calcule o HMAC-SHA256 de `"{X-DF-Timestamp}.{corpo_bruto}"` usando o **secret** do seu webhook e compare, em tempo constante, com o header `X-DF-Assinatura`.

<Warning>
  Use o corpo **bruto** da requisição (raw body), não o JSON já parseado e re-serializado — a ordem das chaves precisa ser idêntica à recebida.
</Warning>

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

  // req.body deve ser o corpo BRUTO (ex.: express.raw)
  function verificar(req, secret) {
    const assinatura = req.header("X-DF-Assinatura");
    const timestamp = req.header("X-DF-Timestamp");
    const corpo = req.body.toString("utf8");

    const esperado = "sha256=" + crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${corpo}`)
      .digest("hex");

    return crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(esperado));
  }
  ```

  ```php PHP theme={null}
  function verificar(string $corpo, array $headers, string $secret): bool
  {
      $assinatura = $headers['X-DF-Assinatura'] ?? '';
      $timestamp  = $headers['X-DF-Timestamp'] ?? '';

      $esperado = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpo, $secret);

      return hash_equals($esperado, $assinatura);
  }
  ```

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

  def verificar(corpo: bytes, headers: dict, secret: str) -> bool:
      assinatura = headers.get("X-DF-Assinatura", "")
      timestamp = headers.get("X-DF-Timestamp", "")
      esperado = "sha256=" + hmac.new(
          secret.encode(), f"{timestamp}.".encode() + corpo, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(esperado, assinatura)
  ```
</CodeGroup>

<Tip>
  Responda rapidamente com `2xx`. Processe o evento de forma assíncrona se necessário — respostas lentas contam como falha.
</Tip>

## Entrega e retentativas

* O seu endpoint deve responder com status **`2xx`**. Qualquer outro status (ou timeout) é tratado como falha. Redirecionamentos (`3xx`) **não são seguidos**.
* Falhas **temporárias** (`5xx`, timeout, falha de conexão, `408`, `429`) são reenviadas com **backoff** crescente (segundos → minutos → 1 hora) por algumas tentativas.
* Falhas **permanentes** (`3xx` e demais `4xx` — endereço errado, rota inexistente, método não aceito) **não são retentadas**: a entrega é marcada como falha na primeira resposta.
* Após muitas falhas consecutivas o webhook é **desativado automaticamente** e religado no dia seguinte para uma nova chance.
* Se as falhas se acumularem sem nenhuma entrega bem-sucedida no meio, a desativação vira **definitiva**: você recebe um **e-mail** avisando, e o webhook só volta quando você corrigir o endpoint e reativá-lo no painel ou via `PATCH /v1/webhooks/{id}` (`"ativo": true`). Atualizar a URL também zera o histórico de falhas.
* Cada entrega traz um `X-DF-Entrega` único — armazene-o e ignore duplicatas (a entrega é *at-least-once*; a ordem não é garantida).
* A URL precisa usar **HTTPS** e apontar para um host público.

## Cota de entregas

Cada plano inclui uma cota mensal de **entregas bem-sucedidas** (respostas `2xx`). A contagem:

* Conta **apenas entregas com resposta `2xx`** — retentativas por falha do seu endpoint **não** consomem a cota.
* É **compartilhada entre todos os seus webhooks** (somada por conta) e **reseta no dia 1º de cada mês** (UTC).
* Eventos de gerenciamento via API (criar, listar, testar etc.) usam a cota de **requisições** normal, não a de entregas.

Quando a cota mensal se esgota, novos eventos **deixam de ser entregues** até o reset: a entrega é registrada com status `descartada` (motivo *Cota mensal de entregas excedida*) e fica visível em `GET /v1/webhooks/{id}/entregas`. Nenhum `POST` é enviado ao seu endpoint.

Acompanhe o consumo no `meta` de `GET /v1/webhooks`:

```json theme={null}
{
  "meta": {
    "entregas_mes_usadas": 1240,
    "entregas_mes_limite": 20000,
    "entregas_referencia": "202605"
  }
}
```

`entregas_mes_limite` retorna `null` quando ilimitado.

## Gerenciamento via API

Além do painel, você pode gerenciar webhooks pela API (autenticada por API Key):

| Método   | Rota                                   | Descrição                                              |
| -------- | -------------------------------------- | ------------------------------------------------------ |
| `GET`    | `/v1/webhooks`                         | Lista seus webhooks                                    |
| `POST`   | `/v1/webhooks`                         | Cria um webhook (retorna o `secret` **uma única vez**) |
| `GET`    | `/v1/webhooks/{id}`                    | Detalhe de um webhook                                  |
| `PATCH`  | `/v1/webhooks/{id}`                    | Atualiza URL, eventos ou status                        |
| `DELETE` | `/v1/webhooks/{id}`                    | Remove um webhook                                      |
| `POST`   | `/v1/webhooks/{id}/rotacionar-segredo` | Gera um novo `secret`                                  |
| `POST`   | `/v1/webhooks/{id}/testar`             | Enfileira uma entrega de teste                         |
| `GET`    | `/v1/webhooks/{id}/entregas`           | Histórico recente de entregas                          |

### Criar um webhook

```bash curl theme={null}
curl -X POST https://api.dadosfutebol.com.br/v1/webhooks \
  -H "Authorization: Bearer SUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "Produção",
    "url": "https://seu-dominio.com/webhook",
    "eventos": ["partida.ao_vivo", "partida.placar_atualizado", "partida.encerrada"]
  }'
```

```json Resposta theme={null}
{
  "data": {
    "id": 10,
    "nome": "Produção",
    "url": "https://seu-dominio.com/webhook",
    "eventos": ["partida.ao_vivo", "partida.placar_atualizado", "partida.encerrada"],
    "ativo": true,
    "secret": "a1b2c3...«guarde com segurança»"
  },
  "mensagem": "Guarde o secret com segurança — ele não será exibido novamente."
}
```

<Warning>
  O `secret` só aparece na criação e ao rotacionar. Se perdê-lo, gere um novo em `POST /v1/webhooks/{id}/rotacionar-segredo`.
</Warning>

Omita `eventos` (ou envie todos) para receber o catálogo completo.

## Cotas por plano

| Plano          | Webhooks ativos | Entregas / mês      |
| -------------- | --------------- | ------------------- |
| **Free**       | 1               | 500                 |
| **Pro**        | 5               | 20.000              |
| **Enterprise** | Sob demanda     | 200.000 (ajustável) |

Exceder a cota de **endpoints ativos** retorna `HTTP 422` na criação:

```json theme={null}
{
  "erro": "Limite do plano",
  "mensagem": "Seu plano permite no máximo 1 webhook(s) ativo(s). Faça upgrade ou desative um existente."
}
```
