WPPAPI
Blog

Migrar da Z-API para a WPPAPI: os 3 pontos que quebram (e como resolver)

·Equipe WPPAPIz-apimigraçãonode.jswebhooks

Quem sai da Z-API costuma sair pelo mesmo motivo: o preço por instância na escala deixa de fechar, ou o time quer webhooks com garantia de entrega. E quase sempre chega com a mesma dúvida: “vou ter que reescrever a integração inteira?”

Não. A WPPAPI usa deliberadamente o mesmo estilo de URL da Z-API — instância e token no path — e os mesmos nomes de campo no envio de texto. Na prática, trocar a base URL já resolve a maior parte do envio.

Mas existem três pontos que mudam de verdade e que quebram em silêncio se você não olhar antes. É sobre eles que este post é.

O que é literalmente igual

Na Z-API você envia assim:

POST https://api.z-api.io/instances/{id}/token/{token}/send-text
{ "phone": "5511999999999", "message": "Olá" }

Na WPPAPI:

POST https://api.wpp-api.com/instances/{id}/token/{token}/send-text
{ "phone": "5511999999999", "message": "Olá" }

Mesmo path, mesmo corpo, mesmo formato de telefone (só dígitos, com DDI). Se seu código monta a URL a partir de uma variável de ambiente, o primeiro envio funciona trocando três variáveis. O token também pode ir no header Client-Token ou Instance-Token, se você prefere manter a URL limpa.

Ponto 1: a resposta do envio tem outro formato

Este é o que mais pega gente desprevenida, porque não dá erro HTTP — só quebra na hora de gravar o ID da mensagem.

A Z-API devolve os identificadores na raiz (zaapId, messageId, id). A WPPAPI devolve um envelope:

{ "success": true, "result": { ... } }

Se você faz const id = res.data.messageId, isso vira undefined silenciosamente e seu log de auditoria fica vazio. Ajuste todo ponto que lê a resposta de envio antes de virar a chave — é uma busca por zaapId e messageId no projeto.

Ponto 2: mídia tem um endpoint por tipo

A Z-API separa por rota (send-image, send-document/{extension}, send-audio…). A WPPAPI também separa, mas com nomes e campos próprios:

Tipo WPPAPI Corpo
Texto POST .../send-text { phone, message }
Imagem POST .../send-image { phone, image, caption }
Documento POST .../send-file { phone, document, fileName }
Vídeo POST .../send-video { phone, video, caption }
Áudio / PTT POST .../send-voice { phone, audio }
Localização POST .../send-location { phone, latitude, longitude, title }
Contato POST .../send-contact { phone, contacts }
Validar número GET .../check-number?phone=...
QR Code GET .../qr-code
Status da conexão GET .../status

Nos campos de mídia você passa uma URL direta (image: "https://...") ou base64. Se o valor não começa com http, ele é tratado como base64 — o prefixo data:image/png;base64, é removido automaticamente, então os dois formatos funcionam. Para controlar o mimetype e o nome do arquivo, use o objeto file:

{
  "phone": "5511999999999",
  "file": {
    "data": "<base64>",
    "mimetype": "application/pdf",
    "filename": "boleto-agosto.pdf"
  }
}

Limite de 100 MB por arquivo, em todos os planos. Além do básico existem enquete (send-poll), lista (send-list), botões (send-buttons), figurinha (send-sticker), reação (PUT .../reaction), encaminhamento (forward) e download de mídia recebida (GET .../media/{messageId}). A referência completa está nos docs.

Ponto 3: o webhook é outro payload (e assinado)

Aqui não tem atalho: o formato é diferente. A Z-API entrega os campos na raiz, com o texto aninhado em text.message. A WPPAPI entrega um envelope com o evento e os dados em data:

{
  "event": "received",
  "wahaEvent": "message",
  "instanceId": "inst_abc123",
  "timestamp": "2026-08-03T14:20:11.145Z",
  "data": {
    "id": "...",
    "from": "[email protected]",
    "chatId": "[email protected]",
    "body": "Olá",
    "type": "chat",
    "fromMe": false,
    "notifyName": "Maria"
  }
}

Se seu handler já tem regra de negócio dentro dele, não reescreva: traduza na entrada. Um adaptador resolve, e ele também valida a assinatura HMAC — que a WPPAPI envia no header X-WPPAPI-Signature (HMAC-SHA256 em hex do corpo cru):

import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const esperado = crypto
    .createHmac("sha256", process.env.WPP_WEBHOOK_SECRET)
    .update(req.body) // Buffer cru, antes do JSON.parse
    .digest("hex");

  const recebido = req.get("X-WPPAPI-Signature") || "";
  if (
    recebido.length !== esperado.length ||
    !crypto.timingSafeEqual(Buffer.from(recebido), Buffer.from(esperado))
  ) {
    return res.sendStatus(401);
  }

  const evt = JSON.parse(req.body.toString());
  if (evt.event !== "received") return res.sendStatus(200);

  // Traduz para o formato que seu código já entende
  meuHandlerAntigo({
    phone: evt.data.from.split("@")[0],
    senderName: evt.data.notifyName,
    isGroup: evt.data.from.endsWith("@g.us"),
    fromMe: evt.data.fromMe,
    messageId: evt.data.id,
    text: { message: evt.data.body },
  });

  res.sendStatus(200);
});

Duas observações que fazem diferença na prática. Primeiro: valide a assinatura antes de qualquer JSON.parse — se você usa express.json(), o corpo cru já foi consumido e o HMAC nunca vai bater. Segundo: responda 200 rápido e processe em fila. A WPPAPI faz retry automático e manda o que falhou de vez para uma dead-letter queue, o que é bom — mas só é bom se seu endpoint responder rápido e for idempotente por data.id. Se você processa síncrono e demora, vai receber a mesma mensagem de novo. O passo a passo completo está no guia de webhooks em Node.js.

Rollout sem parar a operação

Não vire a chave de uma vez. O caminho que menos dói:

  1. Crie uma instância na WPPAPI e conecte um número secundário. Nada em produção ainda.
  2. Aponte o webhook para um endpoint paralelo e rode os dois formatos em paralelo por alguns dias, comparando o que cada um entregou.
  3. Migre por instância, não por percentual de tráfego. Cada número tem uma sessão própria; migrar metade das mensagens de um mesmo número entre dois provedores só confunde o histórico.
  4. Deixe o número antigo conectado na Z-API até o fim do ciclo pago. Não há custo em manter os dois enquanto você valida.

Um número não pode estar conectado em dois provedores ao mesmo tempo — o WhatsApp derruba a sessão anterior. É por isso que o teste começa com um número secundário.

O que não muda: o risco

Vale a honestidade: WPPAPI e Z-API são as duas APIs não oficiais, conectando via QR Code sem a Cloud API da Meta. Trocar de provedor não reduz risco de banimento em nada. Se seu número foi banido disparando promoção para lista fria, ele vai ser banido de novo — valem as mesmas regras de aquecimento, ritmo e opt-in que detalhamos em como evitar banimento.

O que muda é preço previsível na escala (R$ 70 no Básico, R$ 180 no Pro com 3 instâncias, R$ 45 por adicional), webhooks com HMAC, retry e DLQ, e integração nativa com Chatwoot. Se isso resolve sua dor, o teste custa uma tarde.

Crie uma instância e teste por 3 dias, sem cartão — dá tempo de rodar o adaptador acima com um número secundário antes de decidir.

Coloque em prática

3 dias grátis, sem cartão — API, webhooks e Chatwoot inclusos.

Testar 3 dias grátis