One customer shows as three leads because the record was keyed to a submission, not to a person. Every form fill, every session, every inbound webhook creates its own row. Nothing in that chain asks whether the human behind row 2 is the human behind row 1 — so the same buyer arrives three times and gets counted three times.
The number on your dashboard is a count of events. You have been reading it as a count of people.
Why does one person become three leads?
Because deduplication is a decision somebody has to make, and most stacks never make it. A form tool writes a row on submit. A tracking script writes a row per session. A webhook writes a row per delivery. Each of those is correct in isolation. Nobody is holding the question “is this the same person?” at the moment the row is created.
The reason it is hard is that the answer usually arrives late. When the second row is written, all you have is an email address that may be formatted differently, a phone number in a different shape, and a browser that looks brand new. The identifier that would prove they are the same person — the one they typed — is sitting in two records that nothing is comparing.
So the system does the safe thing and creates a new lead. Three times, and now your funnel has three people in it who are one person with a decision to make.
What actually creates the duplicates?
Six causes, in rough order of how much volume they produce.
A second device. Someone clicks your Meta ad on a phone at lunch, fills half the form, then does it properly on a laptop that evening. Two devices never shared storage, so nothing links them except the email or phone they typed both times. This is the same structural problem as tracking one lead across devices, seen from the counting side rather than the attribution side.
Cleared or capped storage on the same device. The browser identifier that recognised them in March is gone by June — evicted, cleared, or capped by the browser. The person is recognised as new and gets a new row. That mechanism is covered in full in why returning customers show as new visitors; the consequence here is simply an extra lead in the count.
Two email addresses. A work address on the first visit, a personal one at checkout. Deliberate, common, and invisible to any tool keyed on email alone.
Formatting differences in the same identifier. Ava.Chen@Example.com and ava.chen@example.com are the same mailbox. +61 412 345 678, 0412 345 678 and 61412345678 are the same phone. A system comparing raw strings sees six different people where there are two.
Multiple entry points into the same CRM. The website form, a Zapier automation, a booking tool and a CSV import all write leads. Each one has its own idea of what makes a record unique, so the same person can land through two doors and stay as two records forever.
Retried and re-delivered webhooks. An integration that times out and retries produces a second identical payload. Without an idempotency key on the receiving end, that is a duplicate lead with the same email and the same timestamp — the easiest kind to spot and the easiest to prevent.

What does an inflated lead count cost you?
Four things, and only the first one is obvious.
Cost per lead reads low. If you spent $3,000 and your dashboard shows 300 leads, your CPL is $10. If 300 rows are really 210 people, your CPL is $14.29 — a 43% difference on the number you use to decide which campaign lives. Duplicates never distribute evenly across sources either. Retargeting and branded search collect the most repeat visitors, so the channels that duplicate the most look the cheapest, and they get the budget.
Conversion rate reads wrong in both directions. Visitor-to-lead rate is inflated because the numerator is duplicated. Lead-to-sale rate is deflated because one sale is being divided by three leads. A campaign can look good on volume and terrible on close rate purely from duplication.
Sales calls the same person twice. Two reps, two records, two introductory calls, one irritated prospect. This is the cost your team feels first and your reporting never shows.
The ad platform learns from a distorted signal. If you send lead events server-side, three events for one person tells the optimiser that this audience segment converts three times as often as it does. You are paying to find more people who look like a counting artefact.
How many real people are in your lead count right now?
You can answer this in an afternoon with an export and a spreadsheet, no new tooling.
Export your leads for a single month with email, phone, created date and source. Then:
- Lowercase and trim every email address. Count distinct values.
- Strip everything except digits from every phone number, then drop the leading zero or country code so the shapes agree. Count distinct values.
- Compare each distinct count to the raw row count.
Whatever ratio comes back is your floor, not your true duplication rate — this method only catches people who used the same identifier twice. The person with two email addresses is still counted twice, and you have no way to find them in a spreadsheet.
Do the same exercise split by source. The spread between your best and worst source is usually larger than anyone expects, and it is the part that changes budget decisions.
Then check one thing more: take ten customers you know bought, and count how many lead rows each one has. If the median is above one, every per-lead metric you report is diluted by that factor.
What can you fix without changing tools?
Three fixes, in order of effort.
Normalise on the way in, not on the way out. Lowercase and trim every email, and convert every phone to a single canonical format — E.164, the +61412345678 shape — before the record is written. Most duplicate pairs are the same string in different clothes, and normalising at write time removes them permanently instead of chasing them in reports.
Pick one unique key and enforce it at the database level. Email is the usual choice; phone is better if your form requires it, because people mistype email addresses more often than phone numbers. Whichever you pick, make it a constraint, not a nightly cleanup job. Cleanup jobs run after the report already went out.
Give every inbound integration an idempotency key. If your CRM accepts webhooks, require a stable ID on each payload and reject a second delivery with the same ID. That kills the retry class of duplicate entirely, and it is usually an hour of work.
What none of this fixes: the same person with two different email addresses, or the same person who never typed anything on the first visit. Those need identity joins across signals, not better string handling.
Why doesn’t deduplicating your CRM fix the reporting?
Because merging records after the fact destroys the very thing your attribution depends on: which touch came first.
When a CRM merges two contacts, it typically keeps one record’s fields and discards the other’s. The surviving record usually holds the most recent source — the branded search or the Direct visit that closed — and the Meta ad click that created the demand months earlier disappears with the merged row. Your lead count gets more honest and your source report gets less honest at exactly the same moment. That is one of the quieter reasons ad conversions show as Direct in reports that look well-maintained.
The other reason is timing. Reporting is usually pulled before the cleanup runs, so the number the meeting sees is the pre-merge number anyway.
Deduplicating a person’s records is the right instinct. It only works if the merge unions their history — every session, every source, every touch, kept — rather than picking a winner and deleting the rest.
How does PartialLeads count one customer once?
By resolving identity before the count, and keeping every touch when it does. PartialLeads captures email and phone as they are typed, normalises both server-side — email lowercased and trimmed, phone to E.164 with a country-code fallback derived from the session’s geo — and then runs a cluster resolver that unions a person’s sessions across six tiers: visitor ID, normalised email, normalised phone, IP plus user-agent within a window, a device fingerprint hash, and a click ID seen on more than one session. Each tier carries a confidence: a visitor-ID echo is treated as certain, email close to it, fingerprint deliberately weakest and bounded to a 90-day window so it cannot mass-merge strangers. How visitor identity resolution works covers the resolver in detail.
Two things follow from that. First, the visitor ID is a first-party cookie set by the server, not written by a script, so the browser cap that shortens script-written cookies does not apply — most same-device duplicates never get created in the first place. Second, because capture happens as the person types rather than on submit, a partial lead already carries the email and phone that let the join happen later. The identifier arrives before the abandonment, not after it.
On the purchase side the same discipline runs one level deeper. Every inbound order goes through one pipeline that normalises identity, deduplicates on client plus source plus order ID plus status phase — so a redelivered webhook physically cannot create a second purchase — and then matches to a session by visitor ID, then email, then phone, then IP, in that order of confidence. Server-side conversion events use a deterministic event_id derived from the pixel, event name and record ID, with a unique constraint behind it, so a retry to Meta or Pinterest is idempotent end to end rather than a second conversion.
Where you see it working: the Journey column on the Leads list renders each touch as a badge left to right with a “N SESSIONS” label, and the ring treatment on each badge encodes which identifier stitched that session into the person — so a three-session lead reads as one row you can verify at list resolution, not three rows you have to reconcile. The Customer Journey timeline shows the same at full detail per lead, and the Attribution report carries first-touch, last-touch and resolved models side by side plus a touches-per-purchase distribution, which is where over-counting shows up as an honest number rather than a suspicion. Reading an attribution report honestly walks through those panels.

The honest constraints. Two email addresses with no shared phone, no shared device and no shared purchase stay two records — the identity graph joins on evidence, and in that case there is none. The fingerprint tier narrows the gap but is capped on purpose, because a resolver that merges aggressively creates a worse error than the one it fixes: two different people in one record. And the resolver does not retroactively invent history for a session that carried no identifier at all. What it does is make sure that when an identifier does arrive, everything that person did before it comes with them.
| What breaks | The mechanism | Where you see it in the dashboard |
|---|---|---|
| A new browser session creates a new lead row | Cluster resolver unions sessions across six tiers before the person is counted | Journey column shows one row with a “N SESSIONS” label instead of three rows |
| Same email in two formats reads as two people | Email lowercased and trimmed, phone normalised to E.164 with geo-based country fallback, server-side | Repeat submissions land on the existing lead, not a new one |
| Same-device visitor lost between visits | Durable visitor ID set as a first-party cookie by the server, localStorage backup, 1-year TTL | Returning sessions attach to the existing lead in the Leads list |
| A retried webhook creates a duplicate purchase | Inbound purchase pipeline deduplicates on client + source + order ID + status phase | Purchases ledger holds one row; refunds net against it rather than duplicating it |
| A retried conversion double-counts at the ad platform | Deterministic event_id (pixel, event name, record ID) behind a unique constraint |
CAPI activity log shows one send per event; the per-lead API column confirms dispatch |
| Merging records loses the first touch | First-touch and last-touch attribution rows written on every matched purchase | Attribution report: first / last / resolved models side by side |
| Multi-visit buyers reported as single-touch | Sessions unioned into the person before attribution is calculated | Touches-per-purchase distribution (1 / 2 / 3 / 4+) |
Tell us what's broken. We'll fix your tracking — free.
Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.
Sources
https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events