Tracking & Attribution

How Do You Attribute Leads That Come Through Live Chat?

Live-chat leads arrive with no source. Why the chat iframe loses the ad click, and how to join a chat lead back to the session that produced it.

Quick answer

A live-chat lead gets its attribution from the browsing session it happened in, not from the chat tool. The chat widget runs in a cross-origin iframe, so it never sees the ad click that brought the visitor in. Push your first-touch UTMs and click IDs into the widget as custom visitor attributes, and join the conversation back to the session on the normalised email or phone the visitor typed. The identifier the person gives you in chat is the only thing both systems reliably share.

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.

A live-chat lead gets its attribution from the browsing session it happened in, not from the chat tool. The chat widget records the conversation; your site records the ad click, the UTM set and the landing page. Join the two on the email or phone the visitor typed in chat, and the campaign that paid for the conversation becomes visible.

That join is the whole job. Everything below is about why it does not happen by default, and what it takes to make it happen reliably.

Why does a live-chat lead arrive with no source?

Because the chat widget runs in a cross-origin iframe owned by the chat vendor. Scripts on your page cannot read what is typed inside it, and the widget cannot read your page’s URL parameters or cookies unless you explicitly hand them over. The conversation and the click end up in two systems that share no identifier at all.

Here is the shape of it. A buyer taps your Meta ad on her phone. She lands on /pricing?utm_source=facebook&utm_campaign=q4-retarget&fbclid=IwAR.... She reads for four minutes, clicks through to /case-studies, then opens the chat bubble and asks whether you integrate with her CRM. She gives her email. Your rep books a call.

Three systems now hold one third of the story each. The chat tool says a visitor on /case-studies started a conversation. Meta says it bought a click and got nothing back. Your CRM has a lead with an email and a rep’s name against it. Nobody has the row that says “this lead came from q4-retarget” — which is exactly why chat-sourced revenue tends to show up as Direct in every report you run.

What does your live-chat tool actually know about the visitor?

Most chat platforms record the page URL the conversation opened on, a referrer string, a device and browser string, a vendor-side visitor cookie, and whatever custom attributes you push into them. What they do not carry by default is the click ID or the UTM set from the landing URL.

The page-URL field fails for the same reason last-click reporting fails: conversations almost never start on the landing page. By the time somebody has enough context to ask a question, they are two or three pages in, and the URL the widget captures is /case-studies — a page that tells you nothing about acquisition.

The vendor’s visitor cookie is no help either. It is first-party to the chat vendor’s domain, not yours, so there is no way to line it up against your own visitor ID. And the fields that would actually settle the question — gclid, fbclid and the UTM parameters — only ever existed on the landing URL, several navigations ago.

How do you pass the ad click into the chat conversation?

Read the click ID and UTMs from the landing URL on the first pageview, store them yourself, then push them into the chat widget as custom visitor attributes when it loads. Every major chat platform supports arbitrary key-value attributes on the visitor record, and those attributes travel with the transcript into your CRM.

The important detail is which values you push. Push the first touch, not the current URL — otherwise you are just re-recording /case-studies in a second place.

// 1. On every pageview: keep the FIRST touch, never overwrite it.
const p = new URLSearchParams(location.search);
const firstTouch = {
  utm_source:   p.get('utm_source')   || '',
  utm_campaign: p.get('utm_campaign') || '',
  gclid:        p.get('gclid')        || '',
  fbclid:       p.get('fbclid')       || '',
  landing_page: location.pathname,
  referrer:     document.referrer || '(direct)'
};

if (!localStorage.getItem('pl_first_touch')) {
  localStorage.setItem('pl_first_touch', JSON.stringify(firstTouch));
}

// 2. When the chat widget is ready, hand it the same values.
//    The method name differs per vendor — check your platform's JS API.
const ft = JSON.parse(localStorage.getItem('pl_first_touch') || '{}');
chatWidget.setVisitorAttributes(ft);

Two practical notes. Set the attributes before the visitor sends their first message, because some platforms snapshot the visitor record when the conversation opens. And keep the key names identical to the column names in your CRM, so nobody has to write a mapping layer later.

This is the same problem class as tracking leads from a Typeform embed: a third-party frame owns the interaction, so attribution has to be handed across the boundary deliberately rather than picked up automatically.

How do you join a chat lead back to its session afterwards?

Use the email or phone as the join key, and normalise both sides the same way before you compare them. Lowercase and trim the email; convert the phone to E.164 with an explicit country code. The identifier the visitor typed into chat is the only value both systems hold, so the join is only as good as the normalisation.

Normalisation is where most home-built joins quietly fail:

  • Ava.Chen@Gmail.com in the chat transcript and avachen@gmail.com in your session table are the same mailbox. Gmail ignores dots and everything after a +.
  • 0412 345 678 and +61412345678 are the same phone. A string comparison says they are not.
  • A visitor who chats from work and later fills your form from home gives you two different sessions and one email — which is a join you want to make, not a duplicate to suppress.

That last case is the interesting one, and it is why a real join happens at the person level rather than the session level. Once several sessions are tied to one person, a conversation that started on /case-studies inherits the campaign that brought them to /pricing last Tuesday. This is the same machinery as visitor identity resolution generally, and the within-visit case — a source-less session sitting next to one that carried the click — is what visit-sibling inheritance repairs.

Diagram of a live-chat lead joined to a browsing session: chat widget iframe on the left with an email address, session record on the right with UTM and click ID fields, connected by a normalised-email join into a single identity cluster

What are the limits of doing this yourself?

The custom-attribute approach works, and it will get you most of the way. It has four failure modes worth knowing before you build a report on top of it.

It only covers pages where your snippet runs first. If the widget initialises before your first-touch code, the attributes arrive empty. Blog pages, help-centre subdomains and anything served by a different template are the usual offenders.

Local storage is not durable. Clear the browser, switch from phone to laptop, or browse in a private window, and the first touch is gone. Safari is stricter still with anything a script writes.

Your CRM has no session table to join against. The transcript arrives with six attribute fields attached, which is better than nothing, but there is nowhere to ask “which other pages did this person see, and when.” You get a label, not a journey.

Nothing goes back to the ad platform. Even with perfect labelling, Meta and Google still have a click with no conversion against it. The report improves; the bidding does not.

How does PartialLeads attribute a live-chat lead?

PartialLeads attributes the session, not the chat. From the first pageview, the tag records the UTM set, the click IDs, the referrer, the landing page and geo against a durable first-party visitor ID. When the email or phone from that conversation reaches PartialLeads — on a form on your site, or on a purchase — the identity graph ties that person to every session they have had, and the campaign behind the chat becomes readable.

The pieces that matter for this problem:

  • The visitor ID survives. pl_vid is set by the server as a first-party cookie with a one-year TTL, with localStorage as a backup. Because the server sets it rather than a script, Safari’s cap on script-written cookies does not apply — the returning visitor stays one person instead of becoming three.
  • The click ID is kept even when the cookie was not. When fbclid is on the landing URL but the _fbc cookie is missing or malformed, the backend reconstructs _fbc in Meta’s documented fb.1.{timestamp}.{fbclid} format, with the URL value as source of truth.
  • The join is six-tier, not one-tier. The cluster resolver unions sessions by visitor ID, normalised email, normalised phone, IP plus user-agent, device fingerprint, and shared click ID — so the chat email lands the person, not just the session they were in when they typed it.
  • Source-less sessions get repaired. Visit-sibling inheritance re-links a session with no attribution to a sibling session within ±30 minutes carrying the same client, IP and user-agent — or within ±10 minutes on a matching fbc cookie when the IP changed, which is what a phone switching from wifi to mobile data looks like.
  • Revenue closes the loop. When the chat lead eventually buys, the purchase is matched to a session — visitor ID echo first, then email, then phone, then IP — and both a first-touch and a last-touch attribution row are written. From there the conversion fans out server-side to Meta, Pinterest, TikTok and Google Ads with SHA-256 hashed email and phone and a deterministic event_id, so a redelivered webhook cannot double-count it.

The honest constraints, because they decide whether this fits your stack. There is no live-chat integration and no chat-transcript ingestion. PartialLeads does not read your conversations, does not create a lead from a chat, and puts no chat event on the journey timeline. The widget is cross-origin, so the tag cannot capture what is typed inside it — the same hard boundary that applies to any third-party embed. What that means in practice: the email has to reach PartialLeads through a path the product does support — a form capture on your site, or a purchase arriving from Shopify, WooCommerce, Stripe, GoHighLevel or the universal webhook. A visitor who only ever chats, never fills a form and never buys leaves you with the session and no join. That is a real gap, not a rounding error, and it is worth knowing before you plan around it.

Where you read the result: the Leads list shows the Journey column as a chain of touch badges with a session count, plus an API column confirming which conversion APIs that specific lead was dispatched to. The Customer Journey timeline gives the same story per person. At the aggregate level the attribution report carries first-touch, last-touch and resolved models side by side, and its Recovered Attribution panel itemises revenue a raw last-click view would have lost — including the “reclassified by session intelligence” bucket, which is where traffic that would otherwise read as Direct ends up.

Leads list mockup showing a chat-sourced lead with a Meta touch badge, a session count and an API column, above an attribution report panel with first-touch, last-touch and resolved model columns

What breaks The mechanism Where you see it in the dashboard
Chat runs in a cross-origin iframe, so the conversation carries no click ID The session holds the attribution: UTMs, gclid/fbclid/msclkid/ttclid/epik, referrer and landing page recorded from the first pageview Leads list — UTM and Location columns
Storage cleared, or Safari drops a script-written cookie Server-set pl_vid first-party cookie, one-year TTL, localStorage backup One lead row instead of three for the same person
The chat opened three pages in, so that session reads Direct Visit-sibling inheritance re-links it to the sibling session carrying the click (±30 min, or ±10 min across a network switch) Journey ribbon on the Leads list
The email from the chat lives in the chat tool, not in your session table Six-tier identity cluster joins on normalised email and phone Customer Journey timeline
Revenue from a chat-sourced buyer reads as Direct Purchase matched to a session, first-touch and last-touch rows written Attribution report — Recovered Attribution
The ad platform never learns the chat lead converted Server-side conversion sent with hashed email and phone and a deterministic event_id CAPI activity log, API column on the lead

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 https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc

Frequently asked questions

QCan a live-chat widget see the UTM parameters on my landing page?
Not on its own. The widget runs in a cross-origin iframe on the vendor's domain, and by the time somebody opens it the visitor is usually several pages past the landing URL anyway. You have to read the UTMs and click IDs yourself on the first pageview, store them, and push them into the widget as custom visitor attributes.
QWhich identifier should I join a chat lead on — email or phone?
Email first, phone as the fallback. Email is the value most chat conversations produce and it is the highest-weight match parameter on the ad platform side too. Normalise before comparing: lowercase and trim the email, and convert phones to E.164 with an explicit country code, or the join silently misses real matches.
QDoes PartialLeads integrate with Intercom, Drift or Crisp?
No. There is no live-chat integration and no chat-transcript ingestion — PartialLeads does not read conversations or create a lead from a chat. What it does is hold the session with its UTMs, click IDs and landing page, so when that person's email arrives on a form or a purchase, the identity graph ties the two together.
QWhat happens if the chat lead never fills out a form and never buys?
You keep the session, but you do not get the join. Without an email or phone reaching PartialLeads through a capture or a purchase record, there is nothing to match the conversation against. In practice most chat leads do eventually hit a form or a checkout, but the ones that do not stay unattributed, and it is better to plan for that than be surprised by it.
QWhy does the session that the chat happened in show as Direct?
Because the click ID and UTMs only ever existed on the landing URL, and internal navigation drops them. Visit-sibling inheritance fixes the common case by re-linking a source-less session to a sibling session from the same visit that did carry the click — same client, IP and user-agent within about thirty minutes, or a matching fbc cookie within ten when the network changed.
QCan I send a chat-sourced conversion back to Meta or Google?
Yes, once the person is matched to a session. The conversion is dispatched server-side with hashed email and phone and a deterministic event ID, so retries and redelivered webhooks cannot double-count. The ad platform still decides whether it can resolve that person to a click — you control the dispatch, the platform controls the matching.
QIs pushing UTMs into chat custom attributes enough on its own?
For labelling, often yes. For measurement, no. Custom attributes give the transcript a source label, but they live in local storage, they break when the visitor switches device, and your CRM has no session table to join them against. You get a label rather than a journey, and nothing goes back to the ad platform to improve bidding.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.