# LGPD, lista de bloqueio e webhook

## Base legal antes de enviar

- Todo envio leva `X-LGPD-Consent-Logged: true` (`--lgpd`): é a empresa declarando que tem base legal para contatar
  cada pessoa (consentimento ou legítimo interesse). Pergunte ao usuário antes de usar; não presuma.
- E-mail em massa pede também `emailLegitimateInterestAck` (veja [disparos.md](disparos.md)).
- A plataforma aplica sozinha: lista de bloqueio, pedido de saída (SAIR, link de descadastro, Meta, pedido na
  ligação), uma abordagem por dia por contato em cada canal e a identificação da empresa no rodapé do e-mail.

## Registrar o consentimento — `POST /v1/lgpd/opt-in-log` · `lgpd.optIn`

Guarda a prova do opt-in (altera: em produção, `--confirmado`).

```json
{ "contact": "5511987654321", "channel": "whatsapp", "source": "formulario-site", "evidence": "Checkbox marcado em 18/09/2026 no formulário de contato.", "collectedAt": "2026-09-18T14:02:11Z" }
```

- `contact`: e-mail ou telefone com DDD. `channel`: `whatsapp`, `sms`, `email` ou `voice`.
- `collectedAt`: quando o consentimento foi dado (ISO 8601, não no futuro). `evidence` até 2.000; `source` até 120.
- Revogar: `{ "contact": "…", "revoked": true, "revokedAt": "2026-10-01T10:00:00Z", "reason": "…" }` (`channel`
  opcional). Depois da revogação, só uma coleta com data posterior volta a valer.

## Lista de bloqueio

- `POST /v1/blocklist/check` (`blocklist.check`, só lê): `{ "contact": "5511987654321" }` → `blocked`. Confira antes
  de montar uma lista.
- `GET /v1/blocklist?page=1` (`blocklist.list`): 500 por página. `contactOptOut: true` = o próprio contato pediu
  para sair.
- `POST /v1/blocklist` (`blocklist.add`; altera): `{ "contact": "…", "reason": "Pediu pelo atendimento para não ser
  mais contatado." }`. Use sempre que a pessoa pedir para não ser contatada.
- `DELETE /v1/blocklist/{blockId}` (`blocklist.remove`; altera): desfaz só um bloqueio feito pela conta. O pedido de
  saída do próprio contato **não se desfaz** (403 `contact_opt_out`) — não tente.

Na sandbox, incluir e remover bloqueio é recusado (403 `test_key_not_allowed`); consultar funciona.

## Webhook de eventos

O webhook avisa o sistema da empresa quando algo acontece, sem ficar consultando. Cadastrar e alterar é
recusado na sandbox; em produção, `--confirmado`.

- `PUT /v1/webhook` (`webhook.set`):
  `{ "url": "https://suaempresa.example/oneclick/webhook", "events": ["message.delivered", "message.replied"], "enabled": true }`
  — `url` só https. Na primeira vez a resposta traz o segredo (`ocs_whsec_…`) **uma única vez**: a pessoa guarda no
  sistema dela; não repita no chat.
- `GET /v1/webhook` (`webhook.get`), `DELETE /v1/webhook` (`webhook.delete`), `POST /v1/webhook/test`
  (`webhook.test`), `POST /v1/webhook/rotate-secret` (`webhook.rotateSecret`, novo segredo na hora) e
  `GET /v1/webhook/deliveries` (`webhook.deliveries`, 7 dias de entregas).
- Eventos: `message.sent`, `message.delivered`, `message.read`, `message.replied`, `message.failed`,
  `message.blocked`, `call.completed`, `contact.blocked`, `payment.confirmed`, `number.disconnected`,
  `campaign.paused` (o teste chega como `webhook.test`).
- Corpo: `{ "id": "evt_…", "type": "<evento>", "createdAt": "…", "data": { … } }`. Cabeçalhos `X-OCS-Event`,
  `X-OCS-Delivery` e `X-OCS-Signature`.
- Entrega: o sistema responde 2xx em até 10 s; senão há novas tentativas (7, por cerca de 20 h). Depois de 20 falhas
  seguidas o webhook é desligado.

### Conferir a assinatura (no sistema que recebe)

`X-OCS-Signature: t=<segundos unix>,v1=<hex>` — HMAC-SHA256, com o segredo, de `"<t>.<corpo bruto>"`. Recuse
diferença de mais de 300 s.

```js
import crypto from 'node:crypto';

export function assinaturaValida(cabecalho, corpoBruto, segredo, agora = Math.floor(Date.now() / 1000)) {
  const partes = Object.fromEntries(String(cabecalho).split(',').map(p => p.split('=')));
  const t = Number(partes.t);
  if (!t || !partes.v1 || Math.abs(agora - t) > 300) return false;
  const esperado = crypto.createHmac('sha256', segredo).update(`${t}.${corpoBruto}`).digest('hex');
  return esperado.length === partes.v1.length && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
}
```

Use o corpo exatamente como chegou (antes de interpretar o JSON).
