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
- Conta no Console wpp-engine:
- Solicite em
/signup(empresa, contato, email, telefone). - Após aprovação, você recebe email + senha temporária (e o link do console).
- Faça login e revele a API key uma única vez em API Keys.
- Solicite em
- Número de WhatsApp que será conectado (celular ou chip dedicado).
- Um endpoint HTTPS público na sua aplicação para receber webhooks (ngrok/localtunnel serve para teste).
- Ferramenta HTTP:
curl, Postman ou código (Node.js abaixo).
Passo 1 — Obter a API key
- Faça login no Console com as credenciais recebidas na aprovação.
- Abra API Keys.
- Clique para revelar a chave (
wae_live_…). - 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:
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):
{
"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.
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:
phoneouphone_number(E.164 com+— o país do+é respeitado; não force55em cima de+1…). Opcionais:display_name,external_id(seu ID interno).
Exemplo de resposta 200:
{
"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:
- Guarde o
session_id. - Abra o
qr_code(é um data URL PNG) no navegador ou no Console (onboarding / tela de sessões). - No celular: WhatsApp → Aparelhos conectados → Conectar um aparelho → escaneie.
Se o QR expirar, peça um novo:
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:
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):
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.
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:
{
"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 eGET /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:
- Upload → recebe
media_id - Envio referenciando esse
media_id
4.1 Upload
# 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:
{
"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)
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
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:
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.
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.
Passo 6 — Receber mídia inbound (ex.: áudio → Whisper)
Quando o cliente envia um áudio/imagem, o webhook traz:
{
"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):
- Validar HMAC no raw body.
- Responder
200imediato. - Se
data.media_idestiver presente:- Preferir
GET /v1/media/{media_id}para obtersigned_urlfresco (não depender só do URL do webhook após horas). - Baixar os bytes e enviar ao Whisper / seu pipeline.
- Preferir
- Dedupe por
event.id/X-WPP-Engine-Event-Id.
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
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
timingSafeEqualno raw body. - Deduplicação por
event.id/X-WPP-Engine-Event-Id.
Confiabilidade
- Responder
200rápido no webhook; processar Whisper/CRM em fila. - Usar
idempotency_keyem todo envio que possa ser retried. - Tratar
429com backoff baseado emRetry-After. - Tratar
session.disconnected/session.banned(pause envios; tentePOST .../reconnectantes de alertar humano). - Watchdog de webhook:
GET /v1/tenants/meperiódico;PATCHsó em drift e semwebhook_secret. - Para
message.failed, usereason(recipient_not_on_whatsapp|session_disconnected|media_error|unknown) e loguereason_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 omedia_idno envio. - Inbound: use
media_id+GET /v1/media/{id}(STT); não confie só nosigned_urlantigo do webhook. - Telefones sempre E.164 com
+completo (+55…BR,+1…EUA, etc.) — sem forçar55em 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 eventogroup.*, não emmessage.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.
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.