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

# Webhooks

> Receba eventos assinados de consentimento e publicação de políticas no seu backend

Webhooks são a forma do **ConsentFly empurrar** eventos para o seu backend em tempo real, em vez de você ter que ficar fazendo polling em `/api/v1/consents`. Ideal para integrar com CDPs, ferramentas de analytics, pipelines de opt-out e qualquer sistema que precise reagir imediatamente quando um titular muda de ideia.

> **Gerenciamento é via dashboard.** Endpoints, secrets e tentativas de reentrega são geridos em `https://www.consentfly.com.br/dashboard/webhooks`. Esta página documenta apenas o **lado consumidor** — o que seu servidor recebe e como validar a assinatura.

## Catálogo de eventos

| Tipo               | Quando dispara                                                                          | Payload (`data`)                         |
| ------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------- |
| `consent.created`  | Banner público registra um consentimento OU `POST /consents` é chamado                  | `ConsentDTO`                             |
| `consent.updated`  | `PUT /consents/{id}` mantém o status aceito                                             | `ConsentDTO`                             |
| `consent.revoked`  | `PUT /consents/{id}` muda `accepted` de `true` para `false`, ou `DELETE /consents/{id}` | `ConsentDTO`                             |
| `consent.erased`   | `DELETE /consents/by-subject/{subjectId}` (DSAR)                                        | `{subject_id, site_id, erased}` agregado |
| `policy.published` | Política criada OU atualizada (cada `version` é um publish)                             | `PolicyResponse`                         |

## Forma do envelope

Cada delivery POST chega no endpoint registrado com este corpo:

```json theme={null}
{
  "id": "evt_<32hex>",
  "type": "consent.created",
  "created_at": "2026-05-15T12:34:56Z",
  "data": { "...": "DTO conforme tabela acima" }
}
```

E estes headers:

```
Content-Type:           application/json
User-Agent:             ConsentFly-Webhooks/1.0
X-ConsentFly-Event:     consent.created
X-ConsentFly-Delivery:  <uuid do delivery>
X-ConsentFly-Signature: t=<unix>,v1=<hex_hmac>
```

## Verificação de assinatura (HMAC-SHA256)

O header `X-ConsentFly-Signature` segue o **mesmo formato do Stripe**: `t=<timestamp>,v1=<hex>`. O valor `v1` é `HMAC_SHA256(secret, "<t>.<corpo_bruto>")`.

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

  function verify(req, rawBody, secret) {
    const header = req.headers["x-consentfly-signature"];
    if (!header) return false;
    const parts = Object.fromEntries(
      header.split(",").map((kv) => kv.split("="))
    );
    if (!parts.t || !parts.v1) return false;

    // Rejeite deliveries com mais de ~5 min de skew para mitigar replay.
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - Number(parts.t)) > 300) return false;

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${parts.t}.${rawBody}`)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(parts.v1, "hex")
    );
  }
  ```

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

  def verify(headers: dict, raw_body: bytes, secret: str) -> bool:
      header = headers.get("X-ConsentFly-Signature")
      if not header:
          return False
      parts = dict(kv.split("=", 1) for kv in header.split(","))
      t, v1 = parts.get("t"), parts.get("v1")
      if not t or not v1:
          return False

      # Rejeite deliveries com mais de ~5 min de skew para mitigar replay.
      if abs(int(time.time()) - int(t)) > 300:
          return False

      payload = f"{t}.".encode() + raw_body
      expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, v1)
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "strconv"
      "strings"
      "time"
  )

  func Verify(header string, rawBody []byte, secret string) bool {
      parts := map[string]string{}
      for _, kv := range strings.Split(header, ",") {
          if idx := strings.IndexByte(kv, '='); idx > 0 {
              parts[kv[:idx]] = kv[idx+1:]
          }
      }
      t, v1 := parts["t"], parts["v1"]
      if t == "" || v1 == "" {
          return false
      }

      // Rejeite deliveries com mais de ~5 min de skew para mitigar replay.
      ts, _ := strconv.ParseInt(t, 10, 64)
      if abs(time.Now().Unix()-ts) > 300 {
          return false
      }

      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write([]byte(t + "."))
      mac.Write(rawBody)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(v1))
  }

  func abs(n int64) int64 {
      if n < 0 {
          return -n
      }
      return n
  }
  ```
</CodeGroup>

**Importante:** use o **corpo bruto** (`raw`) — não o JSON re-serializado pelo seu framework. Re-encoding muda whitespace e quebra a assinatura.

## Política de retry e auto-disable

**Timeout por entrega:** o dispatcher espera no máximo **10 segundos** pela resposta do seu endpoint (incluindo TLS handshake). Qualquer status HTTP 2xx dentro desse limite conta como entrega bem-sucedida; qualquer outra coisa (status não-2xx, timeout, falha de conexão) entra na fila de retry.

| Tentativa       | Espera até a próxima          |
| --------------- | ----------------------------- |
| 1 → 2           | 30s                           |
| 2 → 3           | 2min                          |
| 3 → 4           | 10min                         |
| 4 → 5           | 1h                            |
| 5 → `exhausted` | 6h (último hold, depois para) |

Máximo de 5 tentativas. Após a 5ª falha o delivery vira `exhausted` e só volta a executar via **replay** no dashboard.

No nível do endpoint, **50 falhas consecutivas** disparam auto-disable: o ConsentFly desliga o endpoint, marca `auto_disabled_at` e mostra um pill "Desabilitado automaticamente" no dashboard. Para religar, edite o endpoint pelo painel (isso zera o contador).

## Boas práticas

* **Responda 2xx rápido (\< 10s).** O dispatcher tem timeout de 10 segundos por delivery, incluindo TLS handshake. Se o seu processamento for pesado, enfileire localmente e responda 200 imediato.
* **Idempotência via `X-ConsentFly-Delivery`.** Em caso de retry, o `id` do delivery se repete — use-o como chave de idempotência no seu lado.
* **Aceite somente HTTPS.** O ConsentFly bloqueia URLs `http://` em produção e refuse redirecionamentos 30x — então não tente "redirecionar internamente" o webhook.
* **Não bloqueie IPs.** As entregas saem da infra do ConsentFly (Cloudflare → Railway) e os ranges podem mudar. Se você precisa de allowlist, use a assinatura HMAC como confirmação criptográfica em vez de filtro de IP.

## Caps por plano

| Plano            | Endpoints máximos |
| ---------------- | ----------------- |
| Free / Starter   | 10                |
| Pro / Enterprise | 50                |

A gestão dos endpoints (criar, editar, desabilitar, reenviar deliveries falhadas) acontece **só pelo dashboard** — não há endpoints API-key para isso por enquanto. Se o seu fluxo precisa criar webhooks programaticamente, fale com a gente.

<p align="center">
  Dúvidas? <a href="mailto:suporte@consentfly.com.br">[suporte@consentfly.com.br](mailto:suporte@consentfly.com.br)</a>
</p>
