← blog/5-armadilhas-atribuicao-meta-crmEN
    // blog/5-armadilhas-atribuicao-meta-crm.md

    5 armadilhas da atribuição Meta Ads → CRM que custaram leads de verdade

    Vexo perdeu 83,6% dos leadgens em silêncio por 3 dias. Aqui vão essa e mais quatro pegadinhas servindo PostepTrack em 4 clientes.

    2026-06-03·9 min de leitura·engenharia·POSTEP Digital

    Sleep 10/15s · HTTP 204 trap · try/catch no waitUntil · System User Token · utm_campaign tem dono

    // 00

    A war story

    Cliente: Vexo. Branch: leadgen (Lead Ads do Facebook/Instagram). Período: três dias antes da gente perceber. Resultado: 83,6% dos leads passavam pela Edge Function, eram processados “com sucesso” pela Meta, e simplesmente sumiam sem chegar no Kommo. Sem erro no log. Sem alerta. Só lead que não voltava.

    Quando achamos o motivo, eram dois bugs aninhados num único bloco try. Bem manso de corrigir, devastador de não ver. Esse post junta esse caso e mais quatro que aprendemos servindo PostepTrack em quatro clientes diferentes.

    Se você está construindo (ou contratando) atribuição Meta Ads → CRM, vale ler até o fim. Cada armadilha custou tempo de produção pra gente. Nenhuma está em documentação oficial.

    // 01

    #1 — O CRM demora pra indexar o lead novo

    A Meta entrega o webhook em segundos. O CRM (Kommo, no nosso caso) cria o lead a partir da mensagem do WhatsApp ou do Lead Ad com alguns segundos de atraso adicional — e demora ainda mais pra o lead aparecer no GET /leads. Se você fizer o lookup imediatamente, recebe vazio. E vazio aqui é igual a “lead inexistente”.

    // sleep antes do lookup — número que funciona na prática
    CTWA (Click-to-WhatsApp) 10s
    Leadgen (Lead Ads) 15s
    // menos que isso e a taxa de “lead não encontrado” dispara

    Esses números não são chute. Saíram de medir taxa de sucesso em produção — com 5s rola muito não-encontrado, 10s estabiliza pra CTWA, 15s pra leadgen (porque o lead Ads passa pelo crawler do Facebook antes). Edge Function Supabase precisa de EdgeRuntime.waitUntil pra não bloquear o response do webhook nesse sleep.

    Regra: meça o tempo até o lead estar searchable no SEU CRM. Não use defaults. E nunca faça sleep dentro do handler do webhook sem fire-and-forget — Meta vai chamar de novo achando que você caiu.

    // 02

    #2 — HTTP 204 não é “sucesso”

    Quando você consulta o Kommo procurando um lead por telefone e ele não existe, a API responde com HTTP 204 (No Content). Não 404. Não 200 com array vazio. Só 204 sem corpo.

    // errado — o fetch.ok é true em 204
    const r = await fetch(url);
    if (!r.ok) throw ... // NÃO entra aqui
    const data = await r.json(); // SyntaxError silencioso
    unhandled rejection · lead enriched = false
    // certo — checar 204 explicitamente
    if (r.status === 204) return null;
    ✓ lead inexistente vira fluxo conhecido

    Esse foi o segundo dos dois bugs aninhados no Vexo. r.ok é true pra qualquer status 2xx, incluindo 204. Quando você tenta dar parse no corpo vazio, vem SyntaxError. Sem try/catch externo, vira silent fail. Vezes 83,6% dos leadgens em três dias.

    Regra: trate cada status code que o CRM realmente devolve. 204 não é exceção — é resposta legítima de “não achei”, e seu código tem que saber lidar.

    // 03

    #3 — Fire-and-forget sem try/catch externo

    A Meta espera resposta 200 em poucos segundos. Se você demorar (porque tá esperando o sleep do CRM, fazendo lookup, fazendo PATCH), a Meta dá retry — e seu lead vira duplicado, ou pior, fica em loop infinito de webhook.

    A solução é fire-and-forget: você responde 200 imediatamente e processa o resto em background. No Supabase, isso é EdgeRuntime.waitUntil(asyncTask()). Funciona — mas tem um detalhe que ninguém te avisa.

    // errado — exception some no waitUntil
    EdgeRuntime.waitUntil(processLead(payload));
    return new Response("ok", { status: 200 });
    qualquer throw no processLead → nada aparece no log
    // certo — try/catch externo persiste a falha
    EdgeRuntime.waitUntil((async () => {
    try { await processLead(payload); }
    catch (e) { await logToTable(e); }
    })());
    ✓ exception vira linha visível na tabela de log

    Sem o try/catch externo, qualquer exception dentro do waitUntil vira silent fail. O response 200 já voltou, a Meta tá feliz, mas o processamento morreu em silêncio. Combine isso com o bug do 204 e você tem a história do Vexo.

    Regra: tudo que roda em fire-and-forget precisa de catch externo que escreva numa tabela. Sem isso, você está confiando que nada vai dar errado — e tudo dá errado em produção.

    // 04

    #4 — Token de 90 dias vs System User Token

    A documentação da Meta empurra você pro token de usuário, que expira em 90 dias. Toda renovação é manual — alguém precisa lembrar de logar no Business Manager, gerar token novo, atualizar o env, redeploy. Em quatro clientes, isso é uma armadilha cronológica garantida.

    // dois tipos de token Meta
    User Access Token
    ·validade: 90 dias
    ·renovação manual
    ·vinculado a um usuário humano
    ·quebra se a pessoa sai da empresa
    ·ótimo pra dev e teste
    System User Token
    validade: indefinida
    criado uma vez no Business Manager
    vinculado ao Business, não a pessoa
    sobrevive a troca de equipe
    obrigatório pra produção

    System User Token exige uma permissão específica pra ler campos do leadgen — leads_retrieval. Sem ela, você consulta o lead pelo leadgen_id, recebe 200 com payload vazio, e fica horas debugando seu código que estava certo desde o começo.

    Regra: na hora de ir pra produção, crie System User no Business Manager do cliente, dê leads_retrieval explícito, e use esse token. Esqueça o de 90 dias — ele é pra protótipo.

    // 05

    #5 — utm_campaign quase sempre já tem dono

    Você implementou tudo bonito, o lead chega no Kommo, e quando você popula utm_campaign com o campaign_name vindo da Meta… alguém do marketing reclama que o filtro do dashboard parou de funcionar. Porque utm_campaign já era preenchido por outro sistema upstream — a landing, o formulário, um N8N.

    // padrão observado em 4 clientes PostepTrack
    utm_source → PostepTrack escreve "meta"
    utm_medium → PostepTrack escreve "cpc"
    utm_campaign → 2 de 4 clientes: PRESERVAR
    utm_content → PostepTrack escreve ad_name
    utm_term → PostepTrack escreve adset_name
    // pergunte ANTES de incluir utm_campaign no PATCH

    Em 2 dos 4 clientes que rodamos, utm_campaign já tinha proprietário upstream. PostepTrack agora pula esse campo nesses casos e mapeia campaign_name pra um campo dedicado. Pequeno detalhe, mas evita conversa difícil com o marketing três meses depois.

    Regra: pra cada UTM que você vai escrever no CRM, pergunte se já tem dono. Atribuição é colaborativa — vários sistemas escrevem no mesmo lead. Sobrescrever sem perguntar quebra o trabalho de quem chegou antes.

    // 06

    Resumo: as 5 em uma página

    #1 sleep

    Meça o tempo até o lead ser searchable no CRM. 10s pra CTWA, 15s pra leadgen. Sempre fire-and-forget pra Meta não dar retry.

    #2 HTTP 204

    204 é resposta legítima de “não achei”. r.ok é true em 204. Trate explicitamente antes de dar parse no body.

    #3 waitUntil

    Tudo em fire-and-forget precisa de try/catch externo que persiste a exception numa tabela. Sem isso, exception = silent fail.

    #4 token

    Produção pede System User Token (não expira) + permissão leads_retrieval pra Lead Ads. Token de 90d é só pra protótipo.

    #5 utm_campaign

    Pergunte se já tem dono upstream. Sobrescrever campo de UTM quebra dashboard de quem chegou antes — e ninguém percebe na hora.

    // regra principal

    Atribuição Meta → CRM tem cinco furos onde leads somem em silêncio. Cada um custa tráfego pago real até você ver. Tester só com lead de produção e log que captura exception — não tem outro jeito.

    escrito por
    POSTEP Digital
    ← ver todos os posts