Você gasta R$3 mil em mídia paga no Facebook/Instagram, leads chegam pelo WhatsApp, fecham venda — e o Meta Ads Manager mostra 0 conversões. O CPL parece R$45 no relatório, quando na verdade é R$11. Esse cenário é diário em e-commerces que rodam tráfego pago e fecham via WhatsApp: sem rastreamento server-side, 40-60% das conversões somem do funil.
O motivo é que o Meta Pixel browser-side só captura o que acontece no site. Quando o usuário sai do site, abre WhatsApp e fecha venda lá, o evento de Purchase nunca volta pro Pixel. A solução é Conversions API (CAPI) — você dispara o evento direto do servidor após o webhook de pagamento, com deduplicação contra o Pixel browser. Resultado: Match Quality acima de 7/10, otimização de campanha real, e CPA cair 30-50% nos primeiros 90 dias.
Pixel browser vs CAPI server-side
Como funciona o Pixel browser
Você cola um snippet JavaScript no <head> do site. Quando o visitante navega, ele dispara fbq("track", "ViewContent"), etc. Limitações:
- Bloqueado por ad blockers e iOS 14+ ATT
- Perde eventos quando usuário sai do site (no caso WhatsApp)
- Match quality cai por falta de dados estruturados
Como funciona o CAPI server-side
Seu backend dispara HTTP POST direto pra Graph API com payload assinado. Vantagens:
- Não passa pelo browser — imune a ad blocker
- Captura conversão fora do site (pagamento via webhook PIX/Stripe)
- Permite Advanced Matching com mais sinais (IP, user-agent, fbc, fbp)
Para máximo resultado: rode os dois em paralelo com mesmo event_id — Meta deduplica em 48h e usa o sinal de mais alta qualidade.
Os 5 eventos do funil WhatsApp
- ViewContent: visitante abriu produto/landing page
- Lead: clicou em CTA "Falar no WhatsApp" (sai do site)
- InitiateCheckout: bot WhatsApp gerou cobrança PIX/cartão
- CompleteRegistration: cliente cadastrou (se vende SaaS)
- Purchase: webhook de pagamento confirmou — momento de ouro
Pixel browser: snippet base
<!-- No <head> do site -->
<script>
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'SEU_PIXEL_ID');
fbq('track', 'PageView');
</script>
Em React/Next:
// components/MetaPixel.tsx
"use client";
import Script from "next/script";
export default function MetaPixel() {
return (
<Script id="fb-pixel" strategy="afterInteractive">
{`
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){...
fbq('init', '${process.env.NEXT_PUBLIC_META_PIXEL_ID}');
fbq('track', 'PageView');
`}
</Script>
);
}
Disparo de Lead quando clica no WhatsApp
// Component LinkWhatsApp.tsx
"use client";
import { useState } from "react";
export default function LinkWhatsApp({ telefoneNumero, mensagem }) {
const [clicado, setClicado] = useState(false);
function handleClick() {
const eventId = `lead_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
// Pixel browser
if (typeof window.fbq === "function") {
window.fbq("track", "Lead", { content_name: "whatsapp_cta" }, { eventID: eventId });
}
// Server-side via fetch (próxima seção mostra o backend)
fetch("/api/track-lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
eventId,
url: window.location.href,
userAgent: navigator.userAgent,
fbc: getCookie("_fbc"),
fbp: getCookie("_fbp"),
}),
});
setClicado(true);
window.open(`https://wa.me/${telefoneNumero}?text=${encodeURIComponent(mensagem)}`, "_blank");
}
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? match[2] : null;
}
return (
<button onClick={handleClick} disabled={clicado}>
Falar no WhatsApp
</button>
);
}
Backend: CAPI server-side
import axios from "axios";
import crypto from "crypto";
const META_PIXEL_ID = process.env.META_PIXEL_ID;
const META_CAPI_TOKEN = process.env.META_CAPI_TOKEN;
function hashSHA256(value) {
return crypto.createHash("sha256").update(value.trim().toLowerCase()).digest("hex");
}
function normalizeBRPhone(phone) {
// 11 dígitos -> adiciona 55 -> hash
const digits = phone.replace(/\D/g, "");
const withCountry = digits.startsWith("55") ? digits : `55${digits}`;
return hashSHA256(withCountry);
}
export async function fireMetaEvent({ eventName, eventId, userData, customData = {} }) {
if (!META_PIXEL_ID || !META_CAPI_TOKEN) {
console.warn("META_PIXEL_ID ou META_CAPI_TOKEN não configurados");
return false;
}
const userDataHashed = {};
if (userData.email) userDataHashed.em = [hashSHA256(userData.email)];
if (userData.phone) userDataHashed.ph = [normalizeBRPhone(userData.phone)];
if (userData.firstName) userDataHashed.fn = [hashSHA256(userData.firstName)];
if (userData.lastName) userDataHashed.ln = [hashSHA256(userData.lastName)];
if (userData.externalId) userDataHashed.external_id = [hashSHA256(userData.externalId)];
if (userData.country) userDataHashed.country = [hashSHA256(userData.country)];
// Sinais não-hasheados
if (userData.clientIp) userDataHashed.client_ip_address = userData.clientIp;
if (userData.clientUserAgent) userDataHashed.client_user_agent = userData.clientUserAgent;
if (userData.fbc) userDataHashed.fbc = userData.fbc;
if (userData.fbp) userDataHashed.fbp = userData.fbp;
const payload = {
data: [
{
event_name: eventName,
event_time: Math.floor(Date.now() / 1000),
event_id: eventId,
action_source: "website",
event_source_url: customData.eventSourceUrl,
user_data: userDataHashed,
custom_data: customData,
},
],
};
// Se em modo de teste, adiciona test_event_code
if (process.env.META_TEST_EVENT_CODE) {
payload.test_event_code = process.env.META_TEST_EVENT_CODE;
}
try {
const { data } = await axios.post(
`https://graph.facebook.com/v20.0/${META_PIXEL_ID}/events`,
payload,
{ params: { access_token: META_CAPI_TOKEN }, timeout: 5000 }
);
console.log(`[CAPI] ${eventName} enviado: events_received=${data.events_received}`);
return true;
} catch (err) {
console.error(`[CAPI] erro ${eventName}`, err.response?.data || err.message);
return false;
}
}
Disparo de Purchase no webhook de pagamento
// Quando webhook do gateway confirma pagamento
app.post("/webhooks/pagamento", async (req, res) => {
res.status(200).send("ok"); // ack imediato
if (req.body.event !== "PAYMENT_COMPLETED") return;
const { correlationID, value } = req.body.charge;
const reserva = await db.reserva.findFirst({
where: { correlationID },
include: { cliente: true },
});
if (!reserva) return;
// Marca como pago
await db.reserva.update({
where: { id: reserva.id },
data: { status: "pago", pagaEm: new Date() },
});
// Dispara Purchase no Meta CAPI
// event_id deve ser idêntico ao que o pixel browser usaria (se houver)
const eventId = `purchase_${reserva.id}`;
await fireMetaEvent({
eventName: "Purchase",
eventId,
userData: {
email: reserva.cliente.email,
phone: reserva.cliente.telefone,
firstName: reserva.cliente.nome.split(" ")[0],
lastName: reserva.cliente.nome.split(" ").slice(1).join(" "),
externalId: reserva.cliente.id,
country: "br",
fbc: reserva.cliente.fbc, // capturado no momento do click no link
fbp: reserva.cliente.fbp,
clientIp: reserva.cliente.lastIp,
clientUserAgent: reserva.cliente.lastUserAgent,
},
customData: {
currency: "BRL",
value: value / 100,
content_ids: [reserva.pacoteId],
content_type: "product",
eventSourceUrl: `https://seuapp.com/reserva/${reserva.id}`,
},
});
});
Deduplicação browser+server
Se você dispara Purchase pelo browser (cliente em página de obrigado pós-pagamento) E pelo CAPI (webhook), Meta veria 2 conversões. Solução: mesmo event_id nos dois lados — Meta deduplica em 48h.
// Página de obrigado pós-pagamento (browser)
<script>
fbq("track", "Purchase", {
currency: "BRL",
value: 1499,
content_ids: ["pacote_maragogi_premium"],
}, {
eventID: "purchase_reserva_xyz" // mesmo ID que CAPI vai usar
});
</script>
// Backend
await fireMetaEvent({
eventName: "Purchase",
eventId: "purchase_reserva_xyz", // idêntico
// ...
});
Advanced Matching: subindo Match Quality
Match Quality 0/10 = Meta não consegue casar evento com usuário. Match Quality 8+/10 = otimização robusta. Pra subir:
- Sempre envie email + phone hasheados (ph e em)
- Inclua external_id (seu user_id interno) — ajuda muito
- Capture fbc/fbp no primeiro click e persista no perfil do cliente
- Inclua client_ip_address + client_user_agent no momento do registro
- Hash sempre lowercase e trimmed (Meta exige)
Casos práticos
E-commerce médio: CPA caiu 38%
Antes: Meta Ads CPA reportado R$112. Após CAPI + dedup, CPA real revelado R$71. Algoritmo do Meta passou a otimizar pra essas conversões "novas" (que não conseguia ver antes), CPA continuou caindo nos 90 dias seguintes até R$58.
SaaS B2B: CompleteRegistration de 0.5/10 pra 7.8/10
Adicionou external_id (user_id), fbc/fbp do cookie, IP/UA do request — Match Quality saltou. Custo por registro caiu 41% no Looker.
Agência de viagem: Purchase fora do funil web
Cliente clicou no Insta → falou no WhatsApp → fechou via PIX. Antes: Pixel browser não via Purchase, CPA Insta parecia infinito. Após CAPI no webhook PIX: Purchase aparece com event_id único, otimização de público similar começou a converter mais.
FAQ
Como gerar token CAPI?
Events Manager → seu Pixel → Settings → Generate Access Token. Cria System User no Business Manager se necessário. Token não expira (mas pode ser revogado). Guarde em variável de ambiente, NUNCA no front.
Para que serve test_event_code?
Em Events Manager > Test Events, copie o código (TESTxxxxx). Ao incluir no payload, eventos aparecem em real-time na interface — útil pra debugar antes de ativar produção. Remova depois pra eventos contarem na otimização.
Por quanto tempo Meta deduplica?
48 horas após o primeiro evento. Se browser dispara hoje 14h e CAPI dispara amanhã 15h com mesmo event_id, deduplica. Se passar de 48h, contam dois eventos separados (ruim).
Conversion Lift mostra resultado real?
Sim — é o teste estatístico oficial. Meta divide audience em controle e exposto, mede uplift de conversão atribuível a anúncios. Requer volume mínimo (10k+ conversões/mês). Para campanhas menores, fique em CPA + dedup browser/CAPI.
Signal quality afeta otimização?
Diretamente. Quando signal quality < 5, Meta limita o uso desses sinais pra targeting. Quando > 7, sinais entram em Lookalike Audience, Custom Audience e VOC otimization. Diferença de CPA entre 5 e 8 chega a 35%.
Pronto para captar todas as conversões do seu funil WhatsApp? A ZAP API tem endpoint de webhook que dispara automaticamente em pagamentos confirmados — basta plugar no seu CAPI handler e otimização de mídia melhora em 30 dias. Criar conta grátis e veja exemplos no painel.