# Guia de integração — wpp-engine v1

> **Objetivo:** do zero à primeira mensagem enviada e ao primeiro webhook recebido (texto **e** mídia), no estilo dos quickstarts de Evolution API / Z-API.  
> **Base URL:** `https://wpp-api.oneclient.tech`  
> **Data:** 2026-07-18 (atualizado com upgrade Hermes)

---

## Pré-requisitos

1. Conta no **Console** wpp-engine:
   1. Solicite em [`/signup`](https://wpp-engine-console.vercel.app/signup) (empresa, contato, email, telefone).
   2. Após aprovação, você recebe **email + senha temporária** (e o link do console).
   3. Faça login e revele a API key **uma única vez** em **API Keys**.
2. Número de WhatsApp que será conectado (celular ou chip dedicado).
3. Um endpoint HTTPS público na sua aplicação para receber webhooks (ngrok/localtunnel serve para teste).
4. Ferramenta HTTP: `curl`, Postman ou código (Node.js abaixo).

---

## Passo 1 — Obter a API key

1. Faça login no Console com as credenciais recebidas na aprovação.
2. Abra **API Keys**.
3. Clique para revelar a chave (`wae_live_…`).
4. **Copie e guarde agora** — ela só é mostrada uma vez. Não há recuperação pelo painel.

Se a key for perdida ou comprometida, contate o **suporte OneClient** para rotação. A nova key volta a aparecer **uma vez** no console.

Teste a autenticação:

```bash
export WPP_API_KEY="wae_live_REPLACE_ME"
export WPP_BASE="https://wpp-api.oneclient.tech"

curl -sS "$WPP_BASE/v1/sessions" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

Resposta esperada (lista vazia ou com sessões):

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

Se vier `401` / `INVALID_API_KEY`, a chave está errada ou ainda não foi provisionada.  
Se vier `429` / `RATE_LIMITED`, você estourou o limite (default **60 req/s** por tenant) — respeite o header `Retry-After`.

---

## Passo 2 — Criar uma sessão (conectar o número)

Uma **sessão** = um número WhatsApp = uma **instância**.

```bash
curl -sS -X POST "$WPP_BASE/v1/sessions" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+5511987654321",
    "display_name": "atendimento-clinica-x"
  }'
```

> Aceitos: `phone` **ou** `phone_number` (E.164 com `+` — o país do `+` é respeitado; não force `55` em cima de `+1…`). Opcionais: `display_name`, `external_id` (seu ID interno).

Exemplo de resposta `200`:

```json
{
  "data": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "qr",
    "qr_code": "data:image/png;base64,iVBORw0KGgo...",
    "qr_expires_at": "2026-07-18T12:05:00.000Z"
  },
  "error": null
}
```

**O que fazer com o QR:**

1. Guarde o `session_id`.
2. Abra o `qr_code` (é um data URL PNG) no navegador ou no Console (onboarding / tela de sessões).
3. No celular: WhatsApp → Aparelhos conectados → Conectar um aparelho → escaneie.

Se o QR expirar, peça um novo:

```bash
SESSION_ID="550e8400-e29b-41d4-a716-446655440000"

curl -sS "$WPP_BASE/v1/sessions/$SESSION_ID/qr" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

Confirme o status:

```bash
curl -sS "$WPP_BASE/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

Valores possíveis de `status`: `pending` | `qr` | `pairing` | `connected` | `disconnected` | `banned` | `revoked` | `logged_out`.

Quando conectado, `status` = `connected`. Em produção, prefira o webhook `session.connected` em vez de só fazer polling.

Se a sessão cair depois (`session.disconnected`):

```bash
curl -sS -X POST "$WPP_BASE/v1/sessions/$SESSION_ID/reconnect" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

- Se voltar `status: "connected"` → pronto (auth ainda válida).
- Se voltar `status: "qr"` + `qr_code` → humano precisa escanear de novo.

Quota default: até **50** sessões ativas por tenant (`QUOTA_EXCEEDED_SESSIONS` se passar).

---

## Passo 3 — Enviar a primeira mensagem (texto)

A sessão precisa estar **conectada**. Destino em E.164 **com `+`** (ex.: `+5511987654321` ou `+15617650750`). Números com `+` não recebem `55` automático.

```bash
curl -sS -X POST "$WPP_BASE/v1/sessions/$SESSION_ID/messages" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+5511987654321",
    "content": {
      "type": "text",
      "text": "Olá! Mensagem de teste do wpp-engine."
    },
    "idempotency_key": "teste-primeira-msg-001"
  }'
```

Resposta típica:

```json
{
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "status": "sent",
    "external_message_id": "true_abc123",
    "sent_at": "2026-07-18T12:10:00.000Z"
  },
  "error": null
}
```

- `id` — ID interno da mensagem (use para correlacionar webhooks e `GET /v1/messages/{id}`).
- Confirmações de entrega/leitura chegam por **webhook** (`message.delivered`, `message.read`).
- `idempotency_key` (8–120 chars): a mesma chave **sempre** devolve a mesma mensagem (índice permanente; sem TTL). Ideal para retries.

Se a sessão ainda estiver em QR: `409` / `SESSION_NOT_CONNECTED`.

---

## Passo 4 — Enviar imagem, áudio, vídeo ou documento

Fluxo em **duas etapas**:

1. Upload → recebe `media_id`
2. Envio referenciando esse `media_id`

### 4.1 Upload

```bash
# multipart/form-data — campo do arquivo: `file`
curl -sS -X POST "$WPP_BASE/v1/media/upload" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -F "file=@./foto.jpg"
```

Resposta esperada:

```json
{
  "data": {
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "mime_type": "image/jpeg",
    "size_bytes": 184320,
    "signed_url": "https://...",
    "expires_at": "2026-07-18T13:00:00.000Z"
  },
  "error": null
}
```

| Erro | Quando |
|------|--------|
| `413` / `MEDIA_TOO_LARGE` | Acima do limite do tipo (imagem 5 MB, áudio/vídeo 16 MB, documento 100 MB; hard cap 100 MB) |
| `400` / `INVALID_MEDIA_MIME` | MIME não aceito |
| `403` / `QUOTA_EXCEEDED_STORAGE` | Storage do tenant esgotado (default 10 GB) |

`signed_url` do upload vale ~**24h**; `GET /v1/media/{id}` regenera.

### 4.1b Alternativa — URL pública (carrossel / CDN)

```bash
curl -sS -X POST "$WPP_BASE/v1/media/upload-url" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.exemplo.com/foto.jpg"}'
```

Mesma resposta do multipart (`id` = `media_id`). Só **HTTPS público**; localhost/IPs privados são bloqueados (anti-SSRF). Erro `502` / `MEDIA_FETCH_FAILED` se o download falhar.

### 4.2 Enviar a mídia

```bash
MEDIA_ID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

curl -sS -X POST "$WPP_BASE/v1/sessions/$SESSION_ID/messages" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"to\": \"+5511987654321\",
    \"content\": {
      \"type\": \"image\",
      \"media_id\": \"$MEDIA_ID\",
      \"caption\": \"Resultado do exame\"
    }
  }"
```

| type | Campos | Comportamento |
|------|--------|----------------|
| `text` | `text` (1–4096) | Texto |
| `image` | `media_id` + caption opcional ≤ 1024 | Imagem |
| `audio` | `media_id` | **Voice note (PTT / bolinha)** via WAHA `sendVoice`. Se o upload não for OGG/Opus (ex.: MP3 do TTS), o engine **converte** para OGG/Opus antes do envio (necessário para download no app mobile). |
| `video` | `media_id` + caption opcional | Vídeo |
| `document` | `media_id` + caption opcional | Arquivo anexado |

MIME aceitos no upload: jpeg/png/webp/gif/heic; audio/mpeg|ogg|opus|mp4|aac|webm|wav|amr|…; video/mp4|webm|…; pdf/doc/docx/xlsx. TTS em MP3 é aceito; a conversão para PTT é automática.

Inbound: voice notes WhatsApp (`.oga` / `audio/ogg; codecs=opus`) geram `media_id` no webhook — use `GET /v1/media/{id}` para STT.

---

## Passo 5 — Configurar o webhook

Registre **uma** URL HTTPS por tenant:

```bash
curl -sS -X PATCH "$WPP_BASE/v1/tenants/me" \
  -H "Authorization: Bearer $WPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://seu-servidor.example/webhooks/wpp",
    "webhook_secret": "um-segredo-forte-min-8-chars"
  }'
```

- Se omitir `webhook_secret`, o engine gera um — **guarde na hora** (a API devolve só um prefixo).
- Detalhes: [Webhooks e eventos](./2026-07-18-webhooks-e-eventos.md).

Eventos do fluxo normal:

| Evento | Quando |
|--------|--------|
| `session.connected` | QR escaneado / sessão live |
| `session.qr_generated` | QR criado/renovado |
| `message.sent` | Envio aceito pelo pipeline |
| `message.delivered` | Chegou no aparelho |
| `message.read` | Lida (checks azuis) |
| `message.failed` | Falha (`reason` enum + `reason_raw`) |
| `message.received` | Alguém respondeu (texto **ou** mídia 1:1) |
| `session.disconnected` | Conexão caiu |
| `session.banned` | Número restringido |

**Grupos:** mensagens de `@g.us` nunca geram `message.received`. Se a sessão foi
criada com `groups.enabled: true` e o grupo está na allowlist, o evento é
`group.message.received` (filtrado por `groups.notify`), mais
`group.message.sent` e `group.digest.ready`. `@broadcast` é sempre descartado.
Detalhes na seção 9 da [Referência API](./2026-07-18-referencia-api-v1.md).

---

## Passo 6 — Receber mídia inbound (ex.: áudio → Whisper)

Quando o cliente envia um áudio/imagem, o webhook traz:

```json
{
  "type": "message.received",
  "data": {
    "message_id": "...",
    "external_message_id": "...",
    "from": "+5511...",
    "to": "+5511...",
    "text": null,
    "content_type": "audio",
    "media_id": "uuid-do-asset",
    "mime_type": "audio/ogg",
    "size_bytes": 12345,
    "signed_url": "https://... (válido ~24h)"
  }
}
```

**Fluxo recomendado (produção):**

1. Validar HMAC no raw body.
2. Responder `200` imediato.
3. Se `data.media_id` estiver presente:
   - Preferir `GET /v1/media/{media_id}` para obter `signed_url` fresco (não depender só do URL do webhook após horas).
   - Baixar os bytes e enviar ao Whisper / seu pipeline.
4. Dedupe por `event.id` / `X-WPP-Engine-Event-Id`.

```bash
curl -sS "$WPP_BASE/v1/media/$MEDIA_ID" \
  -H "Authorization: Bearer $WPP_API_KEY"
```

Alternativa: `GET /v1/messages/{message_id}` também devolve `media_asset_id` e metadados.

---

## Passo 7 — Exemplo completo em Node.js

```javascript
const BASE = "https://wpp-api.oneclient.tech";
const API_KEY = process.env.WPP_API_KEY;

async function api(method, path, body) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json().catch(() => null);
  if (res.status === 429) {
    const retry = res.headers.get("Retry-After") ?? "1";
    throw new Error(`RATE_LIMITED retry_after=${retry}`);
  }
  if (!res.ok) {
    throw new Error(`${res.status} ${JSON.stringify(json?.error ?? json)}`);
  }
  return json;
}

async function main() {
  const created = await api("POST", "/v1/sessions", {
    phone: "+5511987654321",
    display_name: "demo",
  });
  const sessionId = created.data.session_id;
  console.log("session_id:", sessionId, "status:", created.data.status);
  console.log("Escaneie o QR antes de continuar.");

  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const detail = await api("GET", `/v1/sessions/${sessionId}`);
    console.log("poll status:", detail.data.status);
    if (detail.data.status === "connected") break;
  }

  const sent = await api("POST", `/v1/sessions/${sessionId}/messages`, {
    to: "+5511987654321",
    content: { type: "text", text: "hello from wpp-engine" },
    idempotency_key: `demo-${Date.now()}`,
  });
  console.log("message id:", sent.data.id, "status:", sent.data.status);

  await api("PATCH", "/v1/tenants/me", {
    webhook_url: "https://seu-servidor.example/webhooks/wpp",
    webhook_secret: "um-segredo-forte-min-8-chars",
  });
  console.log("webhook configurado");
}

main().catch(console.error);
```

---

## Boas práticas (checklist de implementação)

### Segurança

- [ ] API key só no servidor (nunca no front / app do paciente).
- [ ] Webhook HTTPS + verificação HMAC com `timingSafeEqual` no **raw body**.
- [ ] Deduplicação por `event.id` / `X-WPP-Engine-Event-Id`.

### Confiabilidade

- [ ] Responder `200` rápido no webhook; processar Whisper/CRM em fila.
- [ ] Usar `idempotency_key` em todo envio que possa ser retried.
- [ ] Tratar `429` com backoff baseado em `Retry-After`.
- [ ] Tratar `session.disconnected` / `session.banned` (pause envios; tente `POST .../reconnect` antes de alertar humano).
- [ ] Watchdog de webhook: `GET /v1/tenants/me` periódico; `PATCH` **só** em drift e **sem** `webhook_secret`.
- [ ] Para `message.failed`, use `reason` (`recipient_not_on_whatsapp` | `session_disconnected` | `media_error` | `unknown`) e logue `reason_raw`.

### Mídia e voz

- [ ] Outbound TTS: upload MP3 (ou outro áudio) → `type: "audio"` → bolinha PTT; engine converte para OGG/Opus se necessário (mobile baixa corretamente).
- [ ] Carrossel/imagem por URL: `POST /v1/media/upload-url` → use o `media_id` no envio.
- [ ] Inbound: use `media_id` + `GET /v1/media/{id}` (STT); não confie só no `signed_url` antigo do webhook.
- [ ] Telefones sempre E.164 com `+` completo (`+55…` BR, `+1…` EUA, etc.) — sem forçar `55` em cima de outro país.

### Produto / comercial

- [ ] Grupos só chegam se a sessão foi habilitada (`groups`) e o grupo está na allowlist — e no evento `group.*`, não em `message.received`.
- [ ] Não prometa botões / templates Meta oficiais.
- [ ] Preço por instância: sob consulta; cobrança manual nesta fase.
- [ ] Ler [Capacidades e limites](./2026-07-18-capacidades-e-limites.md).

### Retry do engine (webhooks)

- Timeout: 10s.
- Até 5 tentativas: backoff 60s → 5m → 15m → 1h → 6h.
- Após esgotar: DLQ interna; replay operacional `POST /admin/webhooks/:id/retry`.

---

## Próximos documentos

- [Referência completa da API](./2026-07-18-referencia-api-v1.md)
- [Webhooks e eventos](./2026-07-18-webhooks-e-eventos.md)
- [Capacidades e limites](./2026-07-18-capacidades-e-limites.md)
- [Índice](./README.md)
