An identity graph is a stored map of which identifiers belong to the same person. A first-party visitor ID, a normalised email, a phone number, an ad click ID, a payment customer ID — the graph records that these were observed together, keeps the evidence, and treats them as one record.
Attribution needs one because the click and the conversion almost never arrive under the same identifier. Without a graph, a report can only describe browsers. With one, it describes people.
What does an identity graph actually store?
Two things: identifiers, and the evidence that connects them.
Each identifier is a node — one email, one phone number, one cookie ID. Each connection is an edge, and an edge exists only where something was observed: a form submission carrying both an email and the visitor ID of the browser it was typed in, an order carrying both an email and a payment customer ID.
What it does not store is a person. There is no master record of a human in the middle — only a cluster of identifiers the evidence says belong together, with sessions hanging off each one. “Ava Chen” is a label your dashboard prints on a cluster.
That matters when the graph gets something wrong. A system that stores observations can revisit them; one that stores conclusions cannot.
Which identifiers go into a marketing identity graph?
Only identifiers your own systems observed: what a visitor typed into your form, what your server wrote, what arrived in your own landing page URL or your own payment processor. That restriction is what makes the graph a piece of first-party attribution rather than a third-party data purchase.
What each one is worth:
| Identifier | Where it comes from | What it proves | Durability |
|---|---|---|---|
| First-party visitor ID | A cookie your server sets on your domain | Two sessions, same browser | Months if the server sets it, days if a script does |
| Email address | A form, or an order | Strong — people reuse one email across devices | Until they use a second address |
| Phone number | A form, or an order | Strong, and normalises cleanly to E.164 | Permanent, rarely shared |
| Ad click ID | The landing page URL — fbclid, gclid, msclkid, ttclid, epik |
This session came from one specific ad click | One page load, unless you store it |
| Payment customer ID | Stripe, or your billing system | Every future invoice belongs to this person | Permanent within that processor |
| IP address + user agent | The request itself | Weak — same network, same browser build | Minutes to hours |
| Device fingerprint hash | Browser characteristics | Weak — a last resort | Weeks, and degrading |
The pattern: strong identifiers are things a person gave you, weak ones things you inferred.
The click ID is the only row with real urgency. An email is still an email tomorrow; a gclid exists for one page load and is gone, which is why which click ID each ad platform uses is worth knowing before you need it. Unwritten at first touch, the connection between that person and that campaign exists nowhere else.

What makes a join deterministic instead of probabilistic?
A deterministic join is one where the same value appeared in two places. The person typed ava@example.com into a form on her phone in March and onto a checkout on her laptop in April. Nothing is guessed: two records carry a matching key, and the key came from the person both times.
A probabilistic join is an inference. Same IP, same browser version, same city, similar behaviour, therefore probably the same human. Often right, never proof.
The difference shows when you are wrong. A wrong deterministic join is rare and usually traceable — a shared family email, a receptionist’s number on twenty lead forms. A wrong probabilistic join is silent, and merges two strangers into one customer who appears to have taken a journey neither took.
Which is why the honest design is a ladder, not a switch: deterministic keys first, inference only for what they cannot reach, inferred edges marked as such.
Why does every edge need a confidence score?
Because the two ways a graph fails are not symmetrical, and a score is how you choose which one to risk.
Under-merging leaves one person split across three records. Your lead count inflates, your cost per lead looks better than it is, and your journeys are truncated — exactly why one customer shows up as three leads. Embarrassing, but recoverable: the evidence is still there to join later.
Over-merging is worse. Two people become one, and every conclusion downstream inherits the error — the journey is fiction, the revenue lands on the wrong campaign, a conversion event goes back to an ad platform with somebody else’s email attached. No report shows you this happened.
So confidence is what keeps weak evidence from doing strong things. A mature graph ranks its tiers so the strongest available key wins, and bounds the weak ones with time windows and row caps so a shared office IP cannot absorb an entire company.
What does an identity graph fix that a cookie cannot?
Four things, no matter how well the cookie is implemented.
The device switch. A click on a phone and a purchase on a laptop are two browsers with two cookies and no relationship. Only a shared identifier — the email on the order — joins them.
The long gap. Someone clicks in March and buys in June. The click ID is long gone and its session has ended. If the first touch was written against the person rather than the session, it survives.
The payment processor. Stripe knows a card, a customer and an amount. It does not know a campaign. Joining the payment back to the campaign runs through the customer ID and the email — graph work by definition.
The re-identified anonymous. Someone browses anonymously for two weeks, then fills a form. A cookie-only system has two weeks of unattributed sessions and one lead. A graph attaches those sessions to the person retroactively, because the visitor ID they carried now links to an email.
Each is the same operation: a later identifier claiming earlier evidence. That is all identity resolution is, and how visitor identity resolution works is the step-by-step version.
What breaks an identity graph?
Predictable things, and better known in advance.
Shared identifiers. A family laptop, an office NAT with two hundred people behind one IP, info@company.com on forty enquiry forms. Genuine edges, useless ones — hence the caps.
Unstable IPs. iCloud Private Relay, corporate VPNs and carrier networks replace the IP entirely, so anything built on it degrades. Weight it low rather than fight it, which is what attribution for Private Relay and VPN visitors comes down to.
Two addresses for one person. A work email on the demo request, a personal one on the invoice. Unless a phone number or visitor ID overlaps, those clusters stay separate.
Normalisation drift. Ava.Chen@Gmail.com, ava.chen@gmail.com and ava.chen+newsletter@gmail.com are one mailbox and three strings. Normalise them differently in two places and the join silently fails. Meta’s customer information parameters documentation gives the rules for conversion payloads — lowercase, trim, strip formatting from phone numbers — and the same discipline belongs in your own graph.
Consent. If a visitor declines tracking there is no identifier to store, and there should not be one. A graph that respects consent has gaps, and those gaps are correct.
Is an identity graph the same as a CDP or a CRM?
No, and conflating them leads teams to buy the wrong thing.
A CRM stores records you created deliberately — contacts, deals, notes — and assumes you know who someone is. A CDP unifies customer data for activation: audiences, segments, syncs. An identity graph sits underneath both and answers one question, which identifiers belong to the same person; it is what a CDP relies on when it claims to have unified a profile.
The attribution version is narrower still: identifiers carrying campaign evidence, resolved fast enough to send a conversion back to an ad platform while it still matters.
How does PartialLeads build an identity graph?
PartialLeads maintains an identity graph per account: normalised email, phone, Stripe customer ID and IP mapped to sessions, each with a confidence score. A cluster resolver unions a person’s sessions across six tiers — durable visitor ID, email, phone, IP plus user agent, device fingerprint hash, and a click ID seen on more than one session — with the weak tiers deliberately bounded. The fingerprint tier runs on a 90-day window and is capped at 200 rows, specifically to stop a false merge from spreading.
Three details matter, because they are where graphs usually leak.
The visitor ID is set by the server, not by a script. Browser restrictions cap script-written storage aggressively; a cookie set in the HTTP response is not subject to that cap, so the graph’s most durable node stays durable.
Normalisation happens before anything is stored or hashed. Phone numbers go to E.164, using the session’s geo for a missing country code; Gmail dot and plus variants are collapsed before hashing. Joins fail on formatting far more often than on logic.
Purchases match on a ranked ladder. An inbound order matches a session by visitor ID echo first, then email, then phone, then IP, and an unmatched order is stored as unmatched rather than guessed at — recoverable when identity arrives. For sessions broken mid-visit, visit-sibling inheritance lets a sourceless session take campaign data from a sibling in the same visit.
Where you see it: the Journey column in the Leads list draws each session as a badge, and the ring on each badge encodes how that session was stitched into the person — visitor ID, email, phone, IP plus user agent, fingerprint or click ID.

The honest constraints. A graph joins only the evidence it was given: someone using two email addresses, two devices and no phone number stays two clusters until something overlaps. Weak-tier joins are bounded on purpose, so some real merges are declined rather than risked. And resolving identity on your side is not the same as an ad platform resolving it on theirs — we control what gets sent; whether Meta or Google ties that event to a user depends on their graph.
| What breaks | The mechanism | Where you see it in the dashboard |
|---|---|---|
| One person counted as three leads across devices | Six-tier cluster resolver: visitor ID, email, phone, IP + user agent, fingerprint, click ID | Leads list, Journey column with a sessions count |
| Purchases landing with no campaign attached | Orders matched to a session on a ranked ladder; unmatched orders kept and re-matchable | Purchases ledger, matched vs unmatched |
| Safari and cookie clearing dropping returning visitors | Durable visitor ID written by the server, not by a script | Journey ribbon, sessions joined across weeks |
| Joins failing on formatting rather than logic | Email and phone normalised before storage and hashing | Lead detail, normalised contact fields |
| Attribution lost between two sessions in one visit | Visit-sibling inheritance passes campaign data to the sourceless session | Attribution report, resolved model |
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
- Meta for Developers — Conversions API: https://developers.facebook.com/docs/marketing-api/conversions-api
- Meta for Developers — Customer information parameters (normalisation and hashing rules): https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
- Meta for Developers —
fbpandfbcparameters: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc