The CRM tells Meta which ad leads became real customers, so Meta finds more people who buy, not just more people who fill in forms.
Every pipeline move a rep makes sends a server-side Conversions API event to the account's own Meta pixel: Contact → Prospect → ConversionLead → Purchase, with the deal value. Meta then optimises campaigns for qualified leads and sales, and Ads Manager shows real ROAS instead of zero.
"Our ads bring 500 leads a month but half are junk. Meta keeps finding more of the same people, because the only thing it ever sees is that someone filled in a form."
Every lead is tagged with its source when it is created. The source decides which match signals the CRM can later send back to Meta.
| Source | How it reaches the CRM | Signal that ties it back to the ad |
|---|---|---|
Meta Instant FormMETA_INSTANT_FORM | Meta's leadgen webhook in real time, plus a sync every minute as a safety net | Meta lead_id (Meta's strongest signal for form leads) + hashed phone and email |
Website formWEBSITE_FORM | The site's form posts to the CRM; the browser pixel fires on the page | fbc / fbp cookies; fbclid rebuilt into fbc when the cookie is missing |
Click-to-WhatsApp adWHATSAPP_AD | WhatsApp Cloud API webhook, or the Interakt route | ctwa_clid, the only link back to the ad, since these leads have no browser cookies at all |
| Pabbly, webhooks, portals, CSV, manual | Per-account webhook URL, file import, or typed in by a rep | Hashed phone, email, name, city, state, postcode, gender, date of birth where present |
Campaign, ad set and ad names are resolved from Meta when the lead is created, so every lead in the CRM shows which ad it came from.
LeadReceived for form leads, Lead for sources Meta never sawContactProspectConversionLead, the event Meta's Conversion Leads goal optimises forPurchase + deal value and currencyThat is the default stage map. Each account can remap any of its statuses, including custom ones, to a Meta event in Settings → Meta. Junk, Disqualified and other negative statuses send nothing.
Events go from the CRM's server straight to Meta. No browser is involved, so ad blockers, iOS tracking limits and cookie banners cannot drop them.
Each event carries event_source: crm and lead_event_source: Logam CRM, the shape Meta's CRM funnel expects. Meta joins it to the original lead through lead_id.
The id is fixed per lead and event, never a timestamp. A retried send carries the same id, so Meta counts it once.
When a lead reaches the Purchase status, the rep is asked what the sale was worth. Skipping uses the account's default value, so the status change is never blocked. Before this, every conversion reached Meta as zero and ROAS read nothing.
A Meta Instant Form lead is already counted by Meta the moment the form is submitted. Website leads are counted by the browser pixel. So the CRM never sends Lead for those two sources. It sends the stages that follow, which is the part Meta cannot see.
Lead for form leadsLead is simply not sentThis cannot be fixed with deduplication. Meta's on-Facebook lead is not a dataset event and has no event_id to match against, so the only fix is not to send the second one.
Meta's Conversion Leads optimisation needs at least 60% of form leads to have a CRM event. With only stage events, leads left in New or closed as Junk sent nothing, and coverage sat around 20%. Now every Instant Form lead sends one LeadReceived on arrival. It is a funnel stage, not a new lead, so Ads Manager's lead count does not change. Leads synced more than a week late (such as history pulled in when a form is first connected) do not send it.
An event only helps if Meta can match it to a person. What the CRM sends is normalised the way Meta hashes it, then SHA-256 hashed.
New Delhi → newdelhi). Names drop honorifics (Dr. Rajesh Kumar → rajesh / kumar). Country comes from the dial code.The status change saves first. The CAPI send runs in the background, tracked so it finishes even after the response goes out. A Meta outage never stops a rep updating a lead.
Each send tries up to 3 times with a 10-second timeout. Every attempt is written to CAPILog. A failed send is retried by the cron after 15, 30, 60, 120, 240 and 480 minutes, up to 6 times, then marked abandoned. The fixed event_id makes every retry safe.
Events go only to the pixel or dataset configured on that account. With none set, nothing is sent; there is no shared fallback. One business's conversions never train another's ads.
Access tokens are stored encrypted, and the send refuses to run if a token is still ciphertext. Phone numbers, emails and tokens are never written to logs. Broadcast audience uploads send nothing, because those people did not just convert.
| Step | Where | What it does |
|---|---|---|
| 1. Connect the Facebook Page and pick lead forms | Settings → Meta | Leads start flowing in real time |
| 2. Add the Pixel / Dataset ID and access token | Settings → Meta | Turns CAPI on for this account only |
| 3. Check the stage map | Settings → Meta | Decides which status sends which event; custom statuses can be mapped too |
| 4. Set currency and default purchase value | Settings → Meta | Makes ROAS real when a rep skips the value prompt |
| 5. Map form questions to match fields | Settings → Meta → Form Field Matching | Raises match quality |
6. Put LeadReceived first in the sales funnel | Meta Events Manager | Lets Conversion Leads optimisation count coverage correctly |
Super admins see each account's CAPI health (missing pixel or token, failure rate with Meta's own error, unmapped custom statuses) and can test the pixel live with Check now.
Most businesses running Meta lead ads use one of three setups. The comparison below is by approach, based on how each one works, not on any one vendor.
| What matters | Pixel only | Zapier-style relay to CAPI | Typical CRM with a Meta add-on | Logam CRM |
|---|---|---|---|---|
| Tells Meta which leads became sales | No, sees only the form fill | Only if every stage is wired by hand | Often one "converted" event | Every stage, mapped per account |
| Avoids counting form leads twice | Yes | Often re-sends Lead | Varies | Never sends Lead for form or website leads |
| Reaches the 60% lead coverage Meta needs | No | Rarely | Varies | LeadReceived for every form lead |
| Survives ad blockers and iOS limits | No | Yes | Yes | Yes, server to server |
| Click-to-WhatsApp ad leads | No cookies to track | Usually missed | Rarely supported | Sends ctwa_clid |
| Sale value for ROAS | No | If added by hand | Sometimes | Asked at Purchase, default fallback |
| Retries without duplicates | Not applicable | Retry often creates a second event | Varies | 6 retries, fixed event_id |
| Match quality tuning | Browser data only | Raw fields as typed | Basic hashing | Meta's normalisation + form field mapping + coverage report |
| Extra cost and moving parts | None | Per-task fees, another tool to break | Add-on fees | Built in |
The difference that matters most: the CRM is where reps actually qualify leads, so it is the only place that knows which leads were good. Sending that straight from the CRM, counted once and tied to Meta's own lead_id, is what lets Meta optimise for buyers instead of form-fillers.
Purchase already sent cannot be re-priced. Editing the deal value later updates the CRM's own revenue reports only.LeadReceived. Anything older than a week when it reaches the CRM does not send it, to avoid a burst of stale events.lib/meta-capi.js · services/lead.service.js (lead creation, status change, retry) · lib/constants.js (DEFAULT_STAGE_MAP) · app/api/cron/capi-retry · FEATURES.md §12. The duplicate figures (17,380 vs 10,590) are from the comment in services/lead.service.js recording the 30 days before the fix.