The ad platform reports one number, the form tool another, the CRM a third. The tempting conclusion is that one of them is wrong. Usually they’re counting different things.

The fix is to match records one by one, using IDs you control, and give every difference a reason. Below is a full worked example with fictional records.

Six things that get called “a lead”

CountWhat it isWhere it lives
Logged event deliveryOne conversion event your site or server sent to the ad platformYour own event log (tag manager, server logs)
Platform-credited resultA conversion the platform attributed to a campaign under its own attribution and modeling rulesAds Manager / Google Ads reports
Form submissionOne press of submit, accepted by the form systemForm tool log
CRM contactOne person recordCRM
Graded candidateA contact graded Qualified, Nurture or DisqualifiedCRM grade field
Signed agreementA signed franchise agreement (track people and units separately)CRM deal record

The first two are not the same. Your log shows what you sent. The platform’s report shows what it credited, after its own deduplication, attribution windows and modeling. Fourteen deliveries sent doesn’t mean fourteen leads credited. You reconcile your own log record by record. The platform’s credited total is compared separately, as an aggregate.

The worked example

Fictional franchisor, one week, one campaign. These figures are invented for teaching.

  • Your event log shows 14 event deliveries (browser and server).
  • The form tool shows 12 accepted submissions.
  • The CRM shows 9 new contacts.

Every submission gets a submission_id when the form accepts it. That ID is sent with the event as the business_event_id, and stored in the CRM as an external ID. It’s the join key for everything below.

Rowdelivery_idbusiness_event_idevent_nametransportsubmission_idcrm_contact_idWhat happened
1D101S01LeadbrowserS01C501Clean match
2D102S02LeadbrowserS02C502Clean match
3D103S03LeadbrowserS03C503Browser and server both sent…
3D104S03-srvLeadserverS03C503…but the server sent a different ID (S03-srv), so ID-based deduplication isn’t established for this pair
4D105S04LeadbrowserS04C504Clean match
5D106S05LeadbrowserS05C505Same mismatch as row 3…
5D107S05-srvLeadserverS05C505…second delivery
6D108S06LeadbrowserS06C506Clean match
7D109S07LeadbrowserS07C506Same person resubmitted the next day; the CRM matched them to the existing contact
8D110S08LeadbrowserS08(none)Accepted by the form; the CRM integration returned an error
9D111S09LeadbrowserS09C507Clean match
10D112S10LeadbrowserS10(excluded)Internal test, excluded from the CRM, but sent to the platform as a normal event
11D113S11LeadbrowserS11C508Clean match
12D114S12LeadbrowserS12C509Clean match

Reconciliation:

  • 14 deliveries = 12 submissions + 2 extra server deliveries (rows 3 and 5)
  • 12 submissions = 10 distinct non-test people + 1 resubmission (row 7) + 1 test (row 10)
  • 10 distinct people = 9 CRM contacts + 1 unresolved delivery failure (row 8)

Three things to fix:

  1. Rows 3 and 5: the browser and server events carry different IDs. The pair doesn’t meet the ID match that Meta’s recommended deduplication method relies on, so you can’t assume the platform counted it once. See the next section.
  2. Row 8: the record never reached the CRM. It stays an unresolved failure until it’s recovered. After the integration is repaired, log the recovery as a separate state: recovered contact ID, when, and by whom. Until then, report it as one unresolved failure, not a recovered one.
  3. Row 10: the test was kept out of the CRM, but it still went to the ad platform as a real conversion. Send test events through the platform’s test mode, or exclude them from production delivery, so tests don’t train the campaign.

The resubmission in row 7 is working as designed. Keep both submissions in the log and one person in the CRM.

Deduplication: mapping your log to Meta’s fields

business_event_id is a field in your own log. It means nothing to Meta until you send it in the fields Meta reads. Meta’s documentation (checked 28 Oct 2026) describes two methods:

MethodWhat has to matchNotes
Event ID + event name (Meta’s recommended method)Pixel eventID = Conversions API event_id, and pixel event name = Conversions API event_nameDeduplicated if the duplicates arrive within 48 hours of the first event with that event_id, sent to the same Pixel ID
fbp and/or external_idevent_name plus fbp and/or external_id, used consistentlyThe browser event has to arrive first. Duplicates from the same source (browser-only or server-only) aren’t removed

For this example, send business_event_id (S03) as the pixel’s eventID and as the server’s event_id, with the same event name (Lead) on both. Rows 3 and 5 fail the ID-match condition because the server sent S03-srv and S05-srv, respectively. This log alone does not establish the platform’s eventual credited count.

To check it, use Meta’s event diagnostics or test events tool together with your own log. Don’t use the Ads Manager lead count, because a test submission won’t necessarily show up as an attributed lead. Check attribution reporting separately, afterwards. Other platforms use their own fields; map each one from its own documentation.

Joining records

Join on the submission_id that you stored on both sides. It’s the shared key used in this example. Any stable identifier stored on both sides works the same way. Where an old record lacks one, fall back to email plus submission time and set the confidence by how much else agrees: fallback, high if phone, landing page and campaign also match; fallback, low if only email and an approximate time match; ambiguous if two candidates could fit. Don’t assume you can export every modelled or attributed platform conversion at record level. Most platforms don’t expose that.

How to run this on your own data

  1. Fix the window. Same dates and time zone in every system. Note when you exported, because a CRM sync can lag.
  2. Export record-level data from your event log, the form tool and the CRM, each including submission_id.
  3. Join on submission_id. Use the fallback only where you have to, and label its confidence.
  4. Give every unmatched row a reason from a fixed list: extra delivery, resubmission, test, delivery failure, sync delay, unknown. “Unknown” is allowed and stays visible until it’s investigated.
  5. Compare the platform’s credited total separately. If it differs from your distinct-people count, note the attribution settings and look into it. Some gaps can be explained. Some stay open, with their limits written down.
  6. Fix causes, then retest with a marked test submission.

What to report

Report events and people as separate lines. For the example week:

14 event deliveries; 12 submissions; 10 distinct candidates (9 in the CRM, 1 unresolved delivery failure). Platform-credited results reported separately under its attribution settings.

Then grade them. The candidate in row 8 isn’t in the CRM yet, so only 9 can be graded until that record is recovered. If 4 of the 9 are Qualified, report “4 Qualified of 9 graded; 1 not yet graded (unresolved delivery)”, not “4 of 10”.

Start here

Pick last week. Export the three systems at record level, including submission_id. Fill in the reconciliation log. Every row ending as “unknown” is something to investigate.

Download: reconciliation-log.csv, the 14 delivery rows above with every field.

Related: Source tracking from click to CRM · Qualifying candidates

Source (checked 28 Oct 2026): Meta: Handling duplicate Pixel and Conversions API events

Work through your own numbers

Franchise inquiry leak audit. Enter your own records and assumptions. The tool does not store your inputs; its example figures are fictional.

Put it to work on your recruitment system

Bring your recruitment page and anonymized stage counts to a diagnostic. We’ll identify what to test and how to measure it.

Discuss your franchisor recruitment system

More franchisor guides · Read the research and limitations