# Referência da API v1 — wpp-engine

> **Fonte primária:** código do `wpp-engine` + OpenAPI `https://wpp-api.oneclient.tech/v1/openapi.json` (versão `0.1.0`)  
> **Servers:** `https://wpp-api.oneclient.tech` · `http://localhost:3000` (dev)  
> **Auth scheme:** HTTP Bearer (`bearerAuth` — API Key `wae_live_*`)  
> **Data:** 2026-07-18 (atualizado com upgrade Hermes)

Todas as respostas de sucesso usam o envelope:

```json
{ "data": { ... }, "error": null }
```

Listagens paginadas incluem:

```json
{ "data": [ ... ], "pagination": { "next_cursor": null }, "error": null }
```

Erros:

```json
{ "data": null, "error": { "code": "CODIGO", "message": "descrição humana" } }
```

---

## Convenções gerais

### Headers

| Header | Obrigatório | Valor |
|--------|-------------|--------|
| `Authorization` | Sim (exceto health) | `Bearer wae_live_…` |
| `Content-Type` | Em POST/PATCH JSON | `application/json` |

### Telefone (E.164)

Use formato internacional com `+`, mínimo 8 caracteres nos campos validados pelo schema. Ex.: `+5511987654321`, `+15617650750`.

**Regra importante:** se o número já vier com `+`, o engine **respeita o E.164 completo** e **não** prefixa `55` automaticamente.  
Errado: mandar `+15617650750` e esperar que vire Brasil. Certo: `+15617650750` (EUA) ou `+5562999400110` (Brasil).  
Sem `+`, o normalizer pode assumir contexto BR — prefira sempre E.164 com `+`.

Inbound (JIDs do WhatsApp) também é normalizado para E.164 com `+` (inclui restauração do 9º dígito BR quando aplicável).

### Paginação

Query params:

| Recurso | Params | Defaults |
|---------|--------|----------|
| `GET /v1/sessions` | `cursor` (ISO timestamp da página anterior), `limit` | limit 20, max 100 |
| `GET /v1/sessions/{id}/messages` | `cursor` (base64), `limit`, `direction` | limit 50, max 100 |

A resposta traz `pagination.next_cursor`. Se `null`, acabou a página.

### Idempotência (envio)

Campo opcional `idempotency_key` (string 8–120) em `POST .../messages`. A mesma chave **sempre** devolve a mesma mensagem (índice permanente; sem TTL). Use em retries da sua aplicação.

### Rate limit

São três limites, aplicados nesta ordem. Todos respondem `429` com header `Retry-After` (segundos) e `error.code = RATE_LIMITED`.

| Limite | Chave | Default | Quando você encosta nele |
|--------|-------|---------|--------------------------|
| Por tenant | API key | **60 req/s** | É o limite comercial — o único que uma integração normal enxerga |
| Por IP | IP de origem | **6.000 req/min** | Só em volume muito acima do uso normal (proteção da plataforma) |
| Falhas de autenticação | IP de origem | **20 por 10 min** | Chave errada em loop; acerte a `Authorization` e o contador zera na janela |

Variáveis: `RATE_LIMIT_MAX` / `RATE_LIMIT_WINDOW_MS`, `RATE_LIMIT_IP_MAX` / `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_AUTH_FAILURE_MAX` / `RATE_LIMIT_AUTH_FAILURE_WINDOW_MS`.

O limite de falhas de autenticação é novo: antes, requisição com chave inválida não era contada em lugar nenhum, e dava para varrer chaves sem custo. Se você estiver rotacionando a key, faça o deploy do novo valor de uma vez em vez de deixar processos antigos repetindo a chave velha — o período de graça (abaixo) existe justamente para isso.

Desliga com `RATE_LIMIT_ENABLED=false` (só em emergência).

### Rotação de API key

`POST /v1/tenants/me/rotate-key` gera uma chave nova e mantém a atual válida por um **período de graça de 24h** — dá para trocar a chave nos seus serviços sem janela de indisponibilidade.

```bash
curl -sS -X POST "https://wpp-api.oneclient.tech/v1/tenants/me/rotate-key" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

```json
{
  "data": {
    "api_key": "wae_live_…",
    "api_key_prefix": "wae_live_a1b2c3…",
    "previous_key_valid_until": "2026-09-04T18:00:00.000Z"
  },
  "error": null
}
```

A chave nova aparece **uma única vez** nessa resposta — o engine guarda só o hash. Passado `previous_key_valid_until`, a chave antiga é recusada com `401 INVALID_API_KEY`, mesmo que ainda esteja em algum `.env` esquecido. Rotacionar de novo dentro da graça encerra a graça da penúltima: só existem duas chaves válidas por vez, nunca três.

Roteiro sem downtime: rotacione → publique a chave nova nos seus serviços → confira os logs → não precisa fazer mais nada, a antiga expira sozinha.

### Códigos HTTP recorrentes

| Status | Significado típico |
|--------|--------------------|
| 200 | Sucesso |
| 204 | Sucesso sem corpo (DELETE sessão) |
| 400 | Payload inválido |
| 401 | API key ausente/inválida |
| 403 | Sem permissão / quota |
| 404 | Recurso não encontrado |
| 409 | Conflito de estado (ex.: sessão não conectada) |
| 413 | Payload/arquivo grande demais (upload) |
| 429 | Rate limit (`RATE_LIMITED`) |
| 503 | Serviço indisponível / dependência |

Códigos de erro frequentes:

| code | HTTP | Quando |
|------|------|--------|
| `INVALID_API_KEY` | 401 | Bearer inválido |
| `INVALID_PHONE` | 400 | Telefone inválido na criação de sessão |
| `SESSION_NOT_FOUND` | 404 | `session_id` desconhecido neste tenant |
| `QUOTA_EXCEEDED_SESSIONS` | 403 | Quota de sessões do tenant (default 50) |
| `SESSION_NOT_CONNECTED` | 409 | Envio com sessão em `qr` / desconectada |
| `MEDIA_TOO_LARGE` | 413 | Upload acima do limite do tipo |
| `INVALID_MEDIA_MIME` | 400 | MIME não aceito |
| `QUOTA_EXCEEDED_STORAGE` | 403 | Storage do tenant esgotado |
| `RATE_LIMITED` | 429 | Estourou um dos três limites (tenant, IP ou falhas de auth) |
| `ROTATION_UNAVAILABLE` | 503 | Rotação de key indisponível no momento — a key atual continua valendo |

---

## 1. Health

### `GET /v1/health`

Sem autenticação. Checagem de vida do engine.

**Exemplo de resposta** (observado em produção 2026-07-18):

```json
{
  "status": "ok",
  "version": "0.1.0",
  "uptime_seconds": 57280
}
```

> Este path **não** aparece no OpenAPI publicado; é endpoint operacional. Use para monitoramento.

---

## 2. Sessões

### `POST /v1/sessions` — Criar sessão

Cria sessão e devolve QR para parear o WhatsApp.

**Body (JSON)**

| Campo | Tipo | Obrigatório | Notas |
|-------|------|-------------|--------|
| `phone` | string ≥ 8 | um de `phone` / `phone_number` | E.164 |
| `phone_number` | string ≥ 8 | um de `phone` / `phone_number` | Alias aceito |
| `display_name` | string 1–120 | Não | Nome amigável |
| `external_id` | string 1–120 | Não | Seu ID interno |
| `groups` | objeto | Não | Política de grupos (ver seção 9). Omitido = tudo desligado |

**Resposta 200**

```json
{
  "data": {
    "session_id": "uuid",
    "status": "qr",
    "qr_code": "data:image/png;base64,...",
    "qr_expires_at": "2026-07-18T12:05:00.000Z",
    "groups": {
      "enabled": false,
      "notify": "none",
      "digest": { "enabled": false, "time": "20:00", "timezone": "America/Sao_Paulo" }
    }
  },
  "error": null
}
```

`qr_code` e `qr_expires_at` podem ser `null` em estados posteriores.

**Erros documentados no OpenAPI:** 400, 401, 403, 503.

---

### `GET /v1/sessions` — Listar sessões

**Query:** `cursor` (opcional, timestamp ISO da última página), `limit` (opcional, 1–100, default 20).

**Resposta 200**

```json
{
  "data": [ { } ],
  "pagination": { "next_cursor": null },
  "error": null
}
```

Itens do array: schema aberto (`additionalProperties`) no OpenAPI — trate como objetos de sessão; o detalhe tipado está em `GET /v1/sessions/{id}`.

**Erros:** 401.

---

### `GET /v1/sessions/{id}` — Detalhar sessão

**Path:** `id` (UUID da sessão).

**Resposta 200**

```json
{
  "data": {
    "id": "uuid",
    "external_id": "meu-id-ou-null",
    "phone_normalized": "+5511987654321",
    "display_name": "atendimento",
    "status": "string",
    "tier": "string",
    "connected_at": "ISO8601|null",
    "last_connected_at": "ISO8601|null",
    "last_disconnected_at": "ISO8601|null"
  },
  "error": null
}
```

Valores de `status` (CHECK no schema): `pending` | `qr` | `pairing` | `connected` | `disconnected` | `banned` | `revoked` | `logged_out`.  
Valores de `tier`: `cold` | `warm` | `hot` | `verified` (criação inicia em `cold`).

**Erros:** 401, 404.

---

### `PATCH /v1/sessions/{id}` — Atualizar sessão

Edita `display_name` e a política de grupos depois da criação. É um PATCH real:
campo ausente mantém o valor atual, inclusive dentro de `groups.digest`.

**Body (JSON)**

| Campo | Tipo | Notas |
|-------|------|-------|
| `display_name` | string 1–120 | Opcional |
| `groups` | objeto | Opcional (ver seção 9) |

**Resposta 200:** `{ "data": { "session_id", "display_name", "groups" }, "error": null }`

**Erros:** 400, 401, 404.

---

### `DELETE /v1/sessions/{id}` — Revogar sessão

Desconecta / remove a sessão.

**Resposta:** `204` sem corpo (ou corpo vazio). O console BFF trata resposta vazia como sucesso.

**Erros:** 401, 404.

---

### `GET /v1/sessions/{id}/qr` — Obter / renovar QR

Use quando o QR anterior expirou e a sessão ainda não conectou.

**Resposta 200**

```json
{
  "data": {
    "qr_code": "data:image/png;base64,...",
    "qr_expires_at": "ISO8601"
  },
  "error": null
}
```

**Erros:** 401, 404, 503.

---

### `POST /v1/sessions/{id}/reconnect` — Reconectar sessão

Tenta reconectar sem QR (WAHA reaproveita a auth salva). Se o pareamento caiu, devolve QR novo — sem precisar deletar/recriar a sessão.

**Aceita:** `disconnected`, `logged_out`, `qr`, `pending`, `pairing`.  
**Rejeita:** `connected` → `409 SESSION_ALREADY_CONNECTED`; `revoked` / `banned` → `409 SESSION_NOT_RECONNECTABLE`.

**Resposta 200** (mesmo shape do create):

```json
{
  "data": {
    "session_id": "uuid",
    "status": "connected",
    "qr_code": null,
    "qr_expires_at": null
  },
  "error": null
}
```

Se precisar de QR: `status: "qr"` + `qr_code` data URL. Eventos: `session.connected` ou `session.qr_generated`.

**Erros:** 401, 404, 409, 503.

---

## 3. Mensagens

### `POST /v1/sessions/{id}/messages` — Enviar mensagem

**Body**

| Campo | Tipo | Obrigatório |
|-------|------|-------------|
| `to` | string ≥ 8 | Sim, se não usar `to_group` (E.164, chat 1:1) |
| `to_group` | string terminando em `@g.us` | Sim, se não usar `to` (mutuamente exclusivo com `to`) |
| `content` | objeto discriminado por `type` | Sim |
| `idempotency_key` | string 8–120 | Não |

#### `content` — texto

```json
{ "type": "text", "text": "olá" }
```

- `text`: 1–4096 caracteres.

#### `content` — imagem

```json
{ "type": "image", "media_id": "uuid", "caption": "opcional ≤ 1024" }
```

#### `content` — áudio

```json
{ "type": "audio", "media_id": "uuid" }
```

Entrega como **voice note (PTT)** no WhatsApp (WAHA `sendVoice`), não como arquivo anexado.

Se o `mime_type` do asset não for `audio/ogg` / `audio/opus`, o engine chama WAHA `POST /api/{session}/media/convert/voice` e envia o OGG/Opus resultante. Upload em MP3 (TTS) é suportado; sem essa conversão o app mobile costuma falhar ao baixar a bolinha (Web pode funcionar).

#### `content` — vídeo

```json
{ "type": "video", "media_id": "uuid", "caption": "opcional ≤ 1024" }
```

#### `content` — documento

```json
{ "type": "document", "media_id": "uuid", "caption": "opcional ≤ 1024" }
```

**Resposta 200**

```json
{
  "data": {
    "id": "uuid",
    "status": "sent",
    "external_message_id": "string",
    "sent_at": "ISO8601|null"
  },
  "error": null
}
```

**Erros OpenAPI:** 400, 401, 403, 404, 409, 503.

Confirmações posteriores (`delivered` / `read` / `failed`) chegam via [webhooks](./2026-07-18-webhooks-e-eventos.md).

---

### `GET /v1/sessions/{id}/messages` — Listar mensagens da sessão

**Query:** `cursor` (opcional, base64), `limit` (1–100, default 50), `direction` (opcional).

**Resposta 200 — item**

| Campo | Tipo |
|-------|------|
| `id` | uuid |
| `direction` | string |
| `from_normalized` | string |
| `to_normalized` | string |
| `content_type` | string |
| `content_text` | string \| null |
| `status` | string |
| `created_at` | string |

```json
{
  "data": [
    {
      "id": "uuid",
      "direction": "outbound",
      "from_normalized": "+5511...",
      "to_normalized": "+5511...",
      "content_type": "text",
      "content_text": "olá",
      "status": "sent",
      "created_at": "ISO8601"
    }
  ],
  "pagination": { "next_cursor": null },
  "error": null
}
```

**Erros:** 400, 401.

---

### `GET /v1/messages/{id}` — Buscar mensagem por ID

**Resposta 200 — campos:**

`id`, `session_id`, `external_message_id`, `direction`, `from_normalized`, `to_normalized`, `content_type`, `content_text`, `content` (JSONB), `status`, `status_reason`, `media_asset_id`, `content_media_mime_type`, `content_media_size_bytes`, `sent_at`, `delivered_at`, `read_at`, `failed_at`, `created_at`.

Use `media_asset_id` com `GET /v1/media/{id}` para renovar `signed_url` de mídia inbound.

### Como o console interpreta status / `status_reason`

A página **Messages** do console mapeia 1:1 os campos da API (não usa aliases inventados):

| Campo API | Uso no console |
|-----------|----------------|
| `to_normalized` / `from_normalized` | Coluna FROM/TO |
| `content_text` | Preview + drawer |
| `status` | Coluna STATUS (`queued`→retry visual; `failed`→erro) |
| `status_reason` | Banner + linha **motivo** no drawer |

Labels humanas de `status_reason` (mesmo enum do webhook `message.failed.reason`):

| `status_reason` | Texto |
|-----------------|-------|
| `recipient_not_on_whatsapp` | Destino sem WhatsApp |
| `session_disconnected` | Sessão desconectada |
| `media_error` | Erro de mídia |
| `unknown` / ausente | Motivo não informado pelo engine |

Runbook operacional: [docs/agent-output/2026-07-19-runbook-mensagens-erros.md](../agent-output/2026-07-19-runbook-mensagens-erros.md). Catálogo completo de eventos: [webhooks](./2026-07-18-webhooks-e-eventos.md).

**Erros típicos:** 401, 404.

---

## 4. Mídia

### `POST /v1/media/upload` — Upload de mídia

Faz upload e devolve `id` (`media_id`) para usar no envio.

**multipart/form-data:** campo do arquivo = **`file`** (aceita o primeiro arquivo do multipart).

**MIME types aceitos (upload / outbound):**

| kind | MIMEs | Max default |
|------|-------|-------------|
| image | jpeg, png, webp, gif, heic, heif | 5 MB |
| audio | mpeg, ogg, opus, mp4, aac, webm, wav, amr, 3gpp, application/ogg | 16 MB |
| video | mp4, webm, 3gpp, quicktime | 16 MB |
| document | pdf, doc, docx, xlsx | 100 MB |

**Inbound (voice notes do WhatsApp):** tipicamente `.oga` / `audio/ogg; codecs=opus`. O engine aceita esses formatos (e fallbacks por extensão/MIME declarado) ao persistir `media_id` para STT. Sempre use `media_id` + `GET /v1/media/{id}` — não dependa de URLs internas do WAHA.

Hard cap do body: `MEDIA_MAX_UPLOAD_BYTES` (default 100 MB).

**Resposta 200**

```json
{
  "data": {
    "id": "uuid",
    "mime_type": "image/jpeg",
    "size_bytes": 12345,
    "signed_url": "https://...",
    "expires_at": "ISO8601"
  },
  "error": null
}
```

**Erros:** 400 (`INVALID_MEDIA_MIME`), 401, 403 (`QUOTA_EXCEEDED_STORAGE`), **413** (`MEDIA_TOO_LARGE`).

`signed_url` TTL default **24h** (`MEDIA_SIGNED_URL_TTL_SECONDS=86400`); `GET /v1/media/{id}` **regenera** a URL. Retenção do objeto: `media_retention_days` do tenant (default 90), aplicada pelo worker quando `MEDIA_RETENTION_WORKER_ENABLED=true`.

---

### `POST /v1/media/upload-url` — Upload a partir de URL pública

O engine baixa a mídia **server-side** e devolve o mesmo shape do multipart (útil para carrossel / CDN).

**Body**

```json
{ "url": "https://cdn.exemplo.com/foto.jpg", "file_name": "opcional.jpg" }
```

| Campo | Obrigatório | Notas |
|-------|-------------|--------|
| `url` | Sim | **HTTPS** público; IPs privados / localhost bloqueados (anti-SSRF) |
| `file_name` | Não | Override do nome; senão usa o path da URL |

**Erros:** 400 (`INVALID_MEDIA_URL`, `INVALID_MEDIA_MIME`), 401, 403, 413, **502** (`MEDIA_FETCH_FAILED`).

---

### `GET /v1/media/{id}` — Buscar mídia por ID

Mesmo shape de `data` do upload (`id`, `mime_type`, `size_bytes`, `signed_url`, `expires_at`).

**Erros:** 400, 401, 404.

---

## 5. Tenant / Webhook

### `GET /v1/tenants/me` — Conferir webhook (equivalente a webhook/find)

```json
{
  "data": {
    "id": "uuid",
    "slug": "slug",
    "webhook_url": "https://...|null",
    "webhook_secret_prefix": "umse…|null"
  },
  "error": null
}
```

Nunca devolve o segredo completo. Use no watchdog: se `webhook_url` divergir, faça PATCH; caso contrário, não mexa.

**Erros:** 401, 404.

---

### `PATCH /v1/tenants/me` — Atualizar webhook do tenant

**Body**

| Campo | Tipo | Obrigatório | Notas |
|-------|------|-------------|--------|
| `webhook_url` | URI | **Sim** | HTTPS recomendado |
| `webhook_secret` | string 8–255 | Não | Se **omitido**, **preserva** o segredo atual (só gera se ainda não existir). Envie o campo só quando quiser rotacionar. |

**Resposta 200**

```json
{
  "data": {
    "id": "uuid-do-tenant",
    "slug": "slug-do-tenant",
    "webhook_url": "https://...",
    "webhook_secret_prefix": "umse…"
  },
  "error": null
}
```

**Erros:** 400, 404.

Só existe **1 webhook URL por tenant** no v1. Não há CRUD de múltiplos endpoints. Replay operacional de evento na DLQ: `POST /admin/webhooks/:id/retry` (Bearer do tenant) — não é listagem pública de DLQ.

**Padrão watchdog (anti-drift):** `GET /v1/tenants/me` → se URL ok, não faça nada; se divergir, `PATCH` **sem** `webhook_secret` (idempotente, não rotaciona HMAC).

Detalhes de entrega: [Webhooks e eventos](./2026-07-18-webhooks-e-eventos.md).

---

## 6. Mapa rápido (todos os paths do OpenAPI)

| Método | Path | Resumo |
|--------|------|--------|
| POST | `/v1/sessions` | Criar sessão |
| GET | `/v1/sessions` | Listar sessões |
| GET | `/v1/sessions/{id}` | Detalhar sessão |
| DELETE | `/v1/sessions/{id}` | Revogar sessão |
| GET | `/v1/sessions/{id}/qr` | QR da sessão |
| POST | `/v1/sessions/{id}/reconnect` | Reconectar (com ou sem QR) |
| POST | `/v1/sessions/{id}/messages` | Enviar mensagem |
| GET | `/v1/sessions/{id}/messages` | Listar mensagens |
| GET | `/v1/messages/{id}` | Buscar mensagem |
| POST | `/v1/media/upload` | Upload multipart |
| POST | `/v1/media/upload-url` | Upload por URL pública |
| GET | `/v1/media/{id}` | Buscar mídia |
| GET | `/v1/tenants/me` | Conferir webhook |
| PATCH | `/v1/tenants/me` | Atualizar webhook |
| GET | `/v1/health` | Health (operacional; fora do OpenAPI) |

Extras descobertos pelo console (não são paths deste OpenAPI, mas URLs úteis):

| URL | Uso |
|-----|-----|
| `/v1/openapi.json` | Spec máquina |
| `/v1/docs` | Swagger UI |

---

## 7. O que **não** está nesta API (v1)

Não invente rotas. Hoje **não existem** no OpenAPI:

- Administração de grupos (criar grupo, alterar admins, entrar por convite) — participar de grupos existentes **é** suportado (opt-in por sessão)
- Botões / listas / templates HSM / enquetes
- Localização, contato vCard, reações, presença (“digitando”)
- Consulta pública de DLQ de webhooks (existe retry admin + coluna DLQ)
- Provisionamento de tenant via API pública (use `/signup` no console + aprovação)

**Já implementado no engine (Hermes upgrades 2026-07-18):**

- Rate limit HTTP: `429` + `Retry-After` + `RATE_LIMITED` (default 60 req/s por tenant; desliga com `RATE_LIMIT_ENABLED=false`)
- Rotação de API key: via suporte OneClient (nova key aparece one-shot no console)
- Idempotência permanente por `idempotency_key` (sem TTL)
- Paginação: `cursor` + `limit` (sessions default 20 max 100; messages default 50 max 100)
- `POST /v1/sessions/{id}/reconnect` (tenta sem QR; senão devolve QR)
- `POST /v1/media/upload-url` (URL pública HTTPS, anti-SSRF)
- `GET /v1/tenants/me` + PATCH que **preserva** `webhook_secret` se omitido

Ver matriz completa em [Capacidades e limites](./2026-07-18-capacidades-e-limites.md).

---

## 8. Relação com o Console (BFF)

O console Next.js (`/api/sessions`, `/api/messages/send`, …) **não** é a API pública. Ele:

1. Autentica humano via Supabase.
2. Resolve a API key do tenant no banco.
3. Faz proxy para os mesmos `/v1/*` acima.

Integradores devem falar **só** com `https://wpp-api.oneclient.tech/v1/*`.

---

## 9. Grupos

Grupos são **opt-in por sessão**. Com `groups.enabled: false` (default) todo
inbound `@g.us` continua sendo descartado, exatamente como antes desta feature.
`@broadcast` é sempre descartado.

### Objeto `groups`

| Campo | Tipo | Default | Notas |
|-------|------|---------|-------|
| `enabled` | boolean | `false` | Chave mestra da sessão |
| `notify` | `none` \| `mentions` \| `all` | `none` | `none` = silencioso (só envio via API); `mentions` = webhook só com menção (`@`) ou reply à mensagem do próprio número; `all` = toda mensagem |
| `digest.enabled` | boolean | `false` | Resumo periódico por LLM |
| `digest.time` | `HH:MM` (24h) | `20:00` | Horário local do disparo |
| `digest.timezone` | IANA | `America/Sao_Paulo` | Fuso usado para `time` |

Os três cenários típicos: **silencioso + injeção externa** = `enabled: true` +
`notify: "none"`; **responder quando marcado** = `notify: "mentions"`;
**resumo diário** = `digest.enabled: true`.

Além da política da sessão existe uma **allowlist por grupo**: só grupos
registrados e habilitados são processados. Grupos vistos pela primeira vez são
registrados como descobertos (desabilitados) e não geram webhook.

### Endpoints

| Método | Path | Uso |
|--------|------|-----|
| `GET` | `/v1/sessions/{id}/groups` | Lista grupos do WhatsApp da sessão combinados com a allowlist |
| `PUT` | `/v1/sessions/{id}/groups/{jid}` | Registra/atualiza grupo: `{ enabled, name, notify, digest }` (`notify`/`digest` `null` = herda da sessão) |
| `DELETE` | `/v1/sessions/{id}/groups/{jid}` | Remove da allowlist (204) |
| `GET` | `/v1/sessions/{id}/groups/{jid}/messages` | Histórico capturado (`limit`, `since`, `until`) |
| `GET` | `/v1/sessions/{id}/groups/{jid}/digests` | Resumos gerados (`limit`) |

`{jid}` é o JID do grupo (`120363...@g.us`), URL-encoded.

### Envio a grupo

`POST /v1/sessions/{id}/messages` com `to_group` no lugar de `to`. Requisitos:
sessão conectada, `groups.enabled: true` e grupo habilitado na allowlist. Além
da quota diária do tenant existe um **teto por grupo** (`GROUP_DAILY_SEND_CAP`,
default 20/dia) — estourar devolve `403 GROUP_DAILY_CAP_EXCEEDED`.

Erros específicos: `GROUPS_DISABLED`, `GROUP_NOT_ALLOWED`, `INVALID_GROUP_JID`,
`GROUP_DAILY_CAP_EXCEEDED`.

### Eventos

`group.message.received`, `group.message.sent`, `group.digest.ready` — detalhes
em [Webhooks](./2026-07-18-webhooks-e-eventos.md).

### Retenção

`group_messages` tem retenção curta configurável
(`GROUP_MESSAGES_RETENTION_DAYS`, default 30 dias): volume de grupo é alto e os
dados são de terceiros. O resumo (`group_digests`) permanece.

---

## Links

- [Guia de integração](./2026-07-18-guia-de-integracao.md)
- [Webhooks](./2026-07-18-webhooks-e-eventos.md)
- [Índice](./README.md)
- OpenAPI: https://wpp-api.oneclient.tech/v1/openapi.json
