Vexo lost 83.6% of its leadgens silently for 3 days. Here's that story plus four more pitfalls from running PostepTrack across 4 clients.
Sleep 10/15s · HTTP 204 trap · try/catch in waitUntil · System User Token · utm_campaign has an owner
Client: Vexo. Branch: leadgen (Facebook/Instagram Lead Ads). Window: three days before we noticed. Result: 83.6% of leads went through the Edge Function, were processed “successfully” for Meta, and simply vanished before reaching Kommo. No error in the log. No alert. Just leads that never came back.
When we found the cause, it was two bugs nested inside a single try block. Easy to fix, devastating to miss. This post combines that case and four more we learned running PostepTrack across four different clients.
If you're building (or hiring) Meta Ads → CRM attribution, it's worth reading to the end. Each trap cost us production time. None of them are in official documentation.
Meta delivers the webhook in seconds. The CRM (Kommo, in our case) creates the lead from the WhatsApp message or Lead Ad a few extra seconds later — and takes even longer to make that lead appear in GET /leads. If you do the lookup immediately, you get empty. And empty here means “lead does not exist”.
These numbers aren't guesses. We measured success rate in production — 5s gives plenty of not-founds, 10s stabilizes for CTWA, 15s for leadgen (because the Lead Ad passes through Facebook's crawler first). Supabase Edge Functions need EdgeRuntime.waitUntil so the webhook response isn't blocked by that sleep.
Rule: measure the time until the lead is searchable in YOUR CRM. Don't use defaults. And never sleep inside the webhook handler without fire-and-forget — Meta will retry assuming you crashed.
When you query Kommo searching for a lead by phone and it doesn't exist, the API responds with HTTP 204 (No Content). Not 404. Not 200 with an empty array. Just 204 with no body.
This was the second of the two nested bugs at Vexo. r.ok is true for any 2xx status, including 204. When you try to parse the empty body, you get SyntaxError. Without an outer try/catch, it becomes silent fail. Times 83.6% of leadgens over three days.
Rule: handle every status code your CRM actually returns. 204 isn't an exception — it's a legitimate “not found” response, and your code has to know how to deal with it.
Meta expects a 200 response within seconds. If you delay (because you're waiting on the CRM sleep, doing a lookup, doing a PATCH), Meta retries — and your lead becomes a duplicate, or worse, falls into an infinite webhook loop.
The fix is fire-and-forget: you return 200 immediately and process the rest in the background. On Supabase, that's EdgeRuntime.waitUntil(asyncTask()). It works — but there's a detail nobody warns you about.
Without the outer try/catch, any exception inside waitUntil becomes silent fail. The 200 response already went out, Meta is happy, but processing died in silence. Combine that with the 204 bug and you have the Vexo story.
Rule: anything running in fire-and-forget needs an outer catch that writes to a table. Without it, you're trusting nothing will go wrong — and everything goes wrong in production.
Meta's docs push you toward the user token, which expires in 90 days. Every renewal is manual — someone has to remember to log into Business Manager, generate a new token, update the env, redeploy. Across four clients, that's a guaranteed chronological trap.
System User Tokens need a specific permission to read leadgen fields — leads_retrieval. Without it, you query the lead by leadgen_id, receive 200 with an empty payload, and spend hours debugging code that was right from the start.
Rule: when going to production, create a System User in the client's Business Manager, grant leads_retrieval explicitly, and use that token. Forget the 90-day one — it's for prototypes.
You implemented everything cleanly, the lead lands in Kommo, and when you populate utm_campaign with the campaign_name coming from Meta… someone in marketing complains that their dashboard filter stopped working. Because utm_campaign was already being filled by another upstream system — the landing, the form, an N8N.
In 2 of the 4 clients we run, utm_campaign already had an upstream owner. PostepTrack now skips that field in those cases and maps campaign_name to a dedicated field. Small detail, but it avoids a hard conversation with marketing three months later.
Rule: for every UTM you plan to write into the CRM, ask whether it already has an owner. Attribution is collaborative — several systems write to the same lead. Overwriting without asking breaks the work of whoever was there first.
Measure the time until the lead is searchable in the CRM. 10s for CTWA, 15s for leadgen. Always fire-and-forget so Meta doesn't retry.
204 is a legitimate “not found” response. r.ok is true on 204. Handle it explicitly before parsing the body.
Everything in fire-and-forget needs an outer try/catch that persists the exception to a table. Without it, exceptions equal silent fail.
Production wants a System User Token (no expiry) + leads_retrieval permission for Lead Ads. The 90-day one is for prototypes only.
Ask whether an upstream owner already exists. Overwriting a UTM field breaks someone else's dashboard — and nobody catches it in real time.
Meta → CRM attribution has five holes where leads vanish silently. Each one costs real paid traffic before you notice. Test with real production leads and a log that captures exceptions — there's no other way.