Email de confirmação de pedido tem taxa de abertura média de 20%. Mensagem WhatsApp tem 98%. A conta é simples: cinco vezes mais clientes leem a confirmação, cinco vezes menos chamados de "meu pedido foi feito?" no SAC, e uma percepção de profissionalismo que custa caro com outras tecnologias.
Este artigo é o passo a passo de como integrar WhatsApp ao WooCommerce em produção, do webhook ao último template de "pedido entregue". O código foi extraído de uma loja real que processa 200 pedidos/dia e roda há 8 meses sem manutenção.
A arquitetura
Três peças, uma direção:
- WooCommerce dispara webhook em eventos como
order.created,order.updated,order.completed - Servidor Node intermediário recebe o webhook, extrai dados (telefone, nome, total, itens) e formata a mensagem
- ZAP API envia a mensagem WhatsApp para o cliente
Por que precisamos do servidor Node no meio? Porque o webhook do WooCommerce manda um JSON gigante com 200+ campos e a ZAP API espera um payload simples. O servidor é o tradutor.
Pré-requisitos
- WooCommerce 6.0+ instalado e funcionando
- Node.js 18+ rodando em algum lugar com IP público (VPS, Vercel, Railway, Fly.io)
- Conta na ZAP API com instância conectada
Passo 1: Servidor Node básico
npm install express axios crypto dotenv
// server.js
import express from "express";
import axios from "axios";
import crypto from "crypto";
const app = express();
app.use(express.json({ limit: "5mb" }));
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1",
headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});
const INSTANCE = process.env.ZAP_INSTANCE;
const WC_SECRET = process.env.WC_WEBHOOK_SECRET;
// Validação de assinatura HMAC do WooCommerce
function validarAssinatura(req) {
const assinatura = req.headers["x-wc-webhook-signature"];
const corpoRaw = JSON.stringify(req.body);
const esperado = crypto
.createHmac("sha256", WC_SECRET)
.update(corpoRaw)
.digest("base64");
return assinatura === esperado;
}
app.post("/webhook/woocommerce", async (req, res) => {
if (!validarAssinatura(req)) {
return res.status(401).send("Assinatura inválida");
}
res.status(200).send("OK"); // ack imediato
// Processamento assíncrono (importante)
processarPedido(req.body, req.headers["x-wc-webhook-topic"])
.catch((err) => console.error("Erro processando pedido:", err));
});
app.listen(3000, () => console.log("Listening on :3000"));
Passo 2: Função de processamento
async function processarPedido(pedido, topic) {
const telefone = normalizarTelefone(pedido.billing.phone);
if (!telefone) {
console.warn("Pedido sem telefone:", pedido.id);
return;
}
const nome = pedido.billing.first_name;
const total = formatarMoeda(pedido.total);
const numeroPedido = pedido.number;
let mensagem;
switch (topic) {
case "order.created":
mensagem = templatePedidoConfirmado({ nome, numeroPedido, total });
break;
case "order.updated":
if (pedido.status === "processing") {
mensagem = templatePagamentoAprovado({ nome, numeroPedido });
} else if (pedido.status === "shipped") {
mensagem = templateEnviado({ nome, numeroPedido, codigo: pedido.tracking_code });
} else if (pedido.status === "completed") {
mensagem = templateEntregue({ nome, numeroPedido });
}
break;
}
if (!mensagem) return;
await ZAP.post(`/instances/${INSTANCE}/messages`, {
type: "text",
to: telefone,
text: mensagem,
});
}
function normalizarTelefone(raw) {
if (!raw) return null;
const apenasDigitos = raw.replace(/\D/g, "");
if (apenasDigitos.length === 11) return "55" + apenasDigitos;
if (apenasDigitos.length === 13 && apenasDigitos.startsWith("55")) return apenasDigitos;
return null;
}
function formatarMoeda(valor) {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(parseFloat(valor));
}
Passo 3: Os 4 templates de mensagem
Template 1: Pedido confirmado
function templatePedidoConfirmado({ nome, numeroPedido, total }) {
return `Olá, ${nome}! 🎉
Recebemos seu pedido #${numeroPedido} no valor de ${total}.
Estamos confirmando o pagamento e em breve avisaremos por aqui mesmo.
Qualquer dúvida, é só responder esta mensagem.`;
}
Template 2: Pagamento aprovado
function templatePagamentoAprovado({ nome, numeroPedido }) {
return `Boa notícia, ${nome}! ✅
O pagamento do pedido #${numeroPedido} foi aprovado.
Já estamos preparando seu produto. Em até 2 dias úteis você recebe o código de rastreio aqui.`;
}
Template 3: Pedido enviado
function templateEnviado({ nome, numeroPedido, codigo }) {
return `${nome}, seu pedido está a caminho! 📦
Pedido: #${numeroPedido}
Rastreio: ${codigo}
Acompanhe pelo site dos Correios ou pela transportadora.`;
}
Template 4: Pedido entregue
function templateEntregue({ nome, numeroPedido }) {
return `Recebemos a confirmação de entrega do seu pedido #${numeroPedido}, ${nome}! 🙌
Esperamos que tenha gostado. Se puder, deixe sua avaliação no site — isso ajuda muito.
Qualquer problema, conta com a gente.`;
}
Passo 4: Configurar o webhook no WooCommerce
No painel WordPress, vá em WooCommerce → Configurações → Avançado → Webhooks. Crie quatro webhooks (um por evento) apontando para a URL do seu servidor Node:
- Evento "Pedido criado" →
https://seu-servidor.com.br/webhook/woocommerce - Evento "Pedido atualizado" → mesma URL
Use o mesmo secret em todos (configure também na variável WC_WEBHOOK_SECRET).
Passo 5: Teste com curl
curl -X POST https://seu-servidor.com.br/webhook/woocommerce \
-H "Content-Type: application/json" \
-H "x-wc-webhook-topic: order.created" \
-H "x-wc-webhook-signature: $(echo -n '{...}' | openssl dgst -sha256 -hmac 'secret' -binary | base64)" \
-d '{
"id": 12345,
"number": "12345",
"total": "199.90",
"billing": {
"first_name": "Maria",
"phone": "11999998888"
},
"status": "pending"
}'
FAQ
Qual formato de telefone o WooCommerce manda?
Depende da configuração da loja. Por padrão vem como o cliente digitou. Recomendamos forçar máscara (99) 99999-9999 no checkout via plugin de validação. A função normalizarTelefone() acima cobre os 3 formatos mais comuns.
Como uso webhook secret no WooCommerce?
O WooCommerce assina cada requisição com HMAC-SHA256 usando a secret que você configura. A função validarAssinatura() compara a assinatura do header com a calculada localmente. Sem isso, qualquer um pode forjar pedidos.
Tenho múltiplas lojas WooCommerce. Como separar?
Use uma instância WhatsApp por loja na ZAP API e mapeie o ID da loja para a instância correspondente no servidor Node. Pode pegar o ID via header customizado ou via subdomínio (loja1.exemplo.com → instância A, loja2.exemplo.com → instância B).
O que fazer se o cliente cancelar o pedido?
Trate o evento order.updated com status cancelled ou refunded e mande mensagem específica. Não ignore — clientes que cancelam e não recebem confirmação ligam pra reclamar.
O servidor Node deu erro 500. Vou perder a mensagem?
O WooCommerce retenta automaticamente em caso de erro 5xx ou timeout (tenta 5 vezes). Se você responder 200 mas o processamento der erro depois, aí sim perde — por isso o exemplo acima usa try/catch e log estruturado para você reprocessar manualmente. Para produção pesada, persista o webhook recebido em fila (Redis/BullMQ) antes de responder 200.
Próximo passo
Crie sua conta e conecte uma instância em 5 minutos. Criar conta grátis.