Contact Attribution
Attribution answers the two questions every growing team asks: "Where did this contact come from?" and "Which channels are actually working?"
Expedify records this automatically for every contact, no matter how it was created — a website form, a WhatsApp message, a phone call, a Meta lead ad, a CSV import, or an API call. No contact is ever left unattributed: when a channel can't provide marketing details (for example, a manual entry), the contact is classified under Offline Sources rather than left blank.
Two facts, never confused
Attribution tracks two independent facts about every contact. Keeping them separate is the whole idea — collapse them into one and you always lose information.
- How the record entered your CRM — the mechanism. A form, a WhatsApp message, an import, the API. This is the Record Source, and it never changes once set.
- Where the person came from, marketing-wise — the origin. Google Ads, an Instagram post, a referral, direct. This is the Source / Medium / Channel, and it evolves as the contact interacts with you.
These are genuinely different. A contact can be created via WhatsApp yet be
attributed to Paid Social — because they tapped a click-to-WhatsApp ad on
Instagram before ever messaging you. Record Source = whatsapp; Channel =
Paid Social. If Expedify only stored one of these, you could answer "how many
leads did WhatsApp create?" or "which ad channel drove them?" — never both.
Record Source = which door they walked through. Source/Channel = what made them walk toward it.
The field groups at a glance
Every contact carries four groups of fields. They look similar but answer different questions, and they update under different rules:
| Group | Fields | Answers | When it changes |
|---|---|---|---|
| Record source | Record Source, Record Source Detail | "How was this record created, and by which specific form / integration / key?" | Written once at creation. Never changes |
| UTM (raw) | UTM Source, UTM Medium, UTM Campaign, UTM Term, UTM Content | "What exact tracking parameters were in the URL they clicked?" | Only when the contact visits your site through a UTM-tagged link. Never invented |
| Acquisition (first touch) | Acquisition Channel, Source, Medium, Campaign, Date, Entry/Exit Landing Page | "How did this person originally find you?" | Set once, at the first known interaction, then protected |
| Latest touch | Source, Medium, Channel | "What was their most recent interaction?" | Updates every time the contact interacts through a new channel or campaign |
The rest of this page walks through each group with real examples, then shows a full customer journey where you can watch the fields fill in over time.
A full customer journey
The clearest way to understand attribution is to follow one person from first click to won deal. Meet Priya.
January — she clicks a Google ad. Priya searches "best CRM for agencies,"
clicks your Google ad, and lands on yoursite.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=q1_brand&gclid=abc123.
She browses but doesn't convert. The tracking pixel
records her as an anonymous visitor.
February — she fills a form. Priya returns (this time by typing your URL directly), reads a case study, and submits your "Book a Demo" form. The moment she submits, her anonymous history links to the new contact:
| Field | Value | Why |
|---|---|---|
| Record Source | web_form | The form created the record |
| Record Source Detail | Book a Demo (form) | The specific form |
| Acquisition Channel | Paid Search | Her first touch was the Google ad |
| Acquisition Source / Medium | google / cpc | From that first tagged visit |
| Acquisition Campaign | q1_brand | The ad campaign |
| Acquisition Entry Landing Page | .../pricing?... | First page of her first session |
| UTM Source / Medium / Campaign | google / cpc / q1_brand | The verbatim tags she arrived with |
| Latest Source / Channel | google / Paid Search | Her most recent touch so far |
Notice the direct February visit did not overwrite her Google origin — more on that below.
March — she messages you on WhatsApp. Priya has a question and messages your business number. Her latest-touch fields move; her acquisition fields don't:
| Field | Before | After |
|---|---|---|
| Acquisition Channel | Paid Search | Paid Search (unchanged — this is how you won her) |
| Latest Channel | Paid Search | |
| Latest Source |
April — a deal is created. Your rep opens a deal for Priya. The deal inherits her acquisition attribution automatically:
| Deal field | Value |
|---|---|
| Acquisition Channel | Paid Search |
| Acquisition Source | |
| Acquisition Campaign | q1_brand |
So when finance later asks "which channel produced this revenue?", the answer — Paid Search — is already on the deal. Priya's story stays intact end to end: created by a form, acquired by Paid Search, last seen on WhatsApp, revenue credited to Paid Search.
Record source (how the record was created)
Two fields capture the creation mechanism. They are written once and never change.
| Field | What it holds | Example |
|---|---|---|
| Record Source | The mechanism, from a fixed list | web_form, whatsapp, meta_lead_ads, voice, api, import, crm_ui, chatbot, signup, callyzer |
| Record Source Detail | The specific instance of that source | The form, the WhatsApp number/integration, the API key name, the team member, the ad integration |
Record Source Detail is an identifier, not always a friendly name. For an integration-created contact it points to the specific integration; for a form it points to the specific form; for an API contact it's the API key name; for a voice contact it's the number that was called. This lets multi-number, multi-form teams segment precisely — for example, "contacts created by our Mumbai WhatsApp line" or "leads from the pricing-page form."
Why store the mechanism separately from the marketing origin? Because they answer different business questions:
- "How many leads did our forms capture last month?" → group by Record Source
- "Which marketing channel drives the most leads?" → group by Acquisition Channel
UTM fields (raw URL parameters)
These hold the verbatim utm_* parameters from the most recent tagged
visit to your website. They come only from real URLs captured by the
tracking pixel or tagged campaign links — they
are the raw evidence, never a calculated value.
| Field | Example | Notes |
|---|---|---|
| UTM Source | google, newsletter | Which site or property sent the visitor |
| UTM Medium | cpc, email | The marketing medium of the link |
| UTM Campaign | summer_sale | Campaign name from the URL |
| UTM Term | crm software | Paid-search keyword (if passed) |
| UTM Content | banner_a | Ad/creative variant (if passed) |
Empty UTM fields are often correct. A contact who arrived via WhatsApp, a phone call, or an import never clicked a tagged URL — so there is nothing to record. Their acquisition story lives in the Acquisition fields instead.
UTM parameters are for external campaign links only — the links in your ads,
emails, and social posts. If your own website's navigation links carry
utm_source=website, every internal click overwrites the visitor's real origin
with "website," and you lose the fact that they actually came from a Google ad.
Expedify defends against this (a self-referential "website" tag is treated as
Direct and cannot overwrite a real marketing source), but the cleanest data
comes from never tagging internal links in the first place.
UTM vs Source: why both exist
A common question: if the URL already has utm_source=google, why is there
also a separate Source field? Because UTMs are the raw input and Source is
the normalized, always-present output:
- UTMs are only present when someone clicked a tagged link. A WhatsApp lead has
no UTMs — but still needs a Source (
whatsapp). - UTMs are verbatim and messy (
utm_source=Google_Ads,utm_source=google,utm_source=goog). Source normalizes these to one clean value (google) so your reports don't fragment. - Source can be derived even without UTMs — from the referring site (arriving
from
google.comwith no tags →google/organic).
So UTMs feed Source when present; Source always has a sensible value regardless.
Acquisition fields (first touch — set once)
| Field | What it holds | Example |
|---|---|---|
| Acquisition Channel | The classified marketing channel of their first known interaction — see channel list | Paid Search |
| Acquisition Source | Normalized origin | google, facebook, whatsapp, import |
| Acquisition Medium | Normalized medium | organic, cpc, messaging, offline |
| Acquisition Campaign | Campaign that acquired them | q1_brand (ad campaign, or your Expedify campaign) |
| Acquisition Date | Timestamp of the first known interaction (UTC) | 2026-01-14 09:22 UTC |
| Acquisition Entry Landing Page | First page of their first website session | .../pricing?utm_source=google... |
| Acquisition Exit Landing Page | Last page of that same first session | .../book-a-demo |
These are written once and then protected. Two protection rules matter:
- Imports and API calls never overwrite an existing acquisition record.
- Explicitly supplied values are locked. If you import contacts with their own acquisition data (say, from a previous CRM), those values are marked as explicit and will never be replaced by automatic attribution.
There is one deliberate exception — the true first touch upgrade: if a contact was created through an offline channel (say, a WhatsApp message) and Expedify later discovers earlier anonymous website visits by the same person, the acquisition fields upgrade to that earlier, truer first touch — for example, the Google ad they clicked before ever messaging you. Locked (explicitly imported) values are never upgraded.
Entry and Exit landing pages are only filled for contacts with website history. A contact created purely from WhatsApp, a phone call, or an import has no page to record, so these stay empty — until (and unless) that person is later matched to real website visits.
Latest-touch fields (always current)
| Field | What it holds |
|---|---|
| Source | Normalized source of the most recent interaction |
| Medium | Normalized medium of the most recent interaction |
| Channel | Classified channel of the most recent interaction |
Latest-touch updates when the contact does something new: visits your site in a new session, sends a WhatsApp message, calls in, or clicks a campaign link.
Two refinements worth knowing:
- Direct visits don't erase a known source. If their last attributed touch was Paid Search, a later direct visit (typed URL, bookmark) keeps Paid Search — so your reports don't dissolve into "Direct" over time. The latest-touch timestamp still updates.
- Rapid repeat touches are de-duplicated. Refreshing a page or submitting the same form twice in quick succession won't spam the timeline with identical touches.
How every contact gets attributed
This is the "what marks what" — how each way a contact can enter Expedify translates into attribution. The Record Source column is the creation mechanism; the Channel / Source / Medium columns are the marketing origin.
| How the contact arrived | Record Source | Acquisition Channel | Source / Medium |
|---|---|---|---|
| Website form/chat, after clicking a tagged link | web_form / chatbot | Classified from the UTMs (e.g. Paid Search, Email) | The UTM values |
| Website form/chat, arriving from a search engine | web_form / chatbot | Organic Search | google (etc.) / organic |
| Website form/chat, arriving from social media | web_form / chatbot | Organic Social | facebook (etc.) / social |
| Website form/chat, from another website | web_form / chatbot | Referral | referring domain / referral |
| Website form/chat, no referrer | web_form / chatbot | Direct | direct / none |
| WhatsApp message from a click-to-WhatsApp ad | whatsapp | Paid Social | facebook / paid_social |
| WhatsApp message (organic or campaign reply) | whatsapp | whatsapp / messaging | |
| Inbound phone call | voice | Voice | call / voice |
| Meta (Facebook/Instagram) lead form | meta_lead_ads | Paid Social | facebook / paid_social |
| Email or SMS campaign click (return visit) | (unchanged) | (updates latest touch) Email / SMS | email / sms |
| CSV import — without attribution columns | import | Offline Sources | import / offline |
| CSV import — with your own attribution columns | import | Classified from your values, then locked | Your values |
| Created manually in the CRM | crm_ui | Offline Sources | crm_ui / offline |
| Created via the API | api | Offline Sources (or classified, if the call includes attribution) | api / offline |
| Created by a workflow | workflow | Offline Sources | workflow / offline |
| New account signup (self-serve) | signup | Classified from signup UTMs, else Direct | signup UTMs, else direct |
Worked example: WhatsApp, three ways
WhatsApp is the best illustration of why Record Source and Channel are separate, because the same record source produces different channels:
| Situation | Record Source | Channel | Source / Medium | Also captured |
|---|---|---|---|---|
| Person taps your click-to-WhatsApp ad on Instagram | whatsapp | Paid Social | facebook / paid_social | The ad's click ID + ad ID |
| Person messages your number organically | whatsapp | whatsapp / messaging | Which WhatsApp number received it | |
| You import your WhatsApp contact book | integration | Offline Sources | whatsapp_business_sync / integration | — |
All three are "WhatsApp" to a human, but the attribution correctly separates a paid-ad acquisition from an organic chat from a bulk sync.
Channel definitions
Acquisition Channel and the latest-touch Channel always come from a fixed list, so your reports stay consistent:
| Channel | Meaning |
|---|---|
| Paid Search | Paid ads on search engines (Google Ads, Bing Ads) |
| Paid Social | Paid ads on social platforms (Meta, LinkedIn, …) — including click-to-WhatsApp ads |
| Paid Shopping / Paid Video / Display / Cross-network | Other paid formats |
| Organic Search | Unpaid search-engine traffic |
| Organic Social / Organic Video / Organic Shopping | Unpaid platform traffic |
| Email links (including your Expedify email campaigns) | |
| SMS | SMS links |
| WhatsApp conversations (organic or your campaigns) | |
| Voice | Inbound phone calls |
| Referral | Links from other websites |
| Affiliates / Audio / Mobile Push Notifications | Specialty sources |
| Direct | Typed URL or bookmark, no known origin |
| Offline Sources | Everything that doesn't come from tracked marketing: imports, manual entry, API, integrations |
| Unassigned | Couldn't be classified (rare) |
Deals inherit attribution
When a deal is linked to contacts, it inherits the acquisition channel, source and campaign of its primary contact (or the first linked contact if none is marked primary). That makes revenue-by-channel reporting a single grouping — every won deal already knows which channel originally produced its contact. Inheritance happens once and is not overwritten, so the deal's origin stays stable even as contacts are added or reordered.
FAQ
What's the difference between Record Source and Source?
Record Source is how the record was created (form, WhatsApp, import, API) — it
never changes. Source is where the person came from, marketing-wise (google,
facebook, direct) — it updates with their latest interaction. A contact created
via a form (Record Source = web_form) can have Source = google because they
arrived through a Google ad. See Two facts, never confused.
Why are the UTM fields empty on my contact? Because that contact never clicked a UTM-tagged link. UTM fields record real URL parameters only — they are never fabricated. Check the Acquisition fields for how the contact was actually acquired.
Why does Acquisition Channel say "Offline Sources"? The contact entered Expedify through a path that carries no marketing signal — an import, manual entry, API call, or integration. If the same person is later identified on your website with earlier visit history, the true-first-touch upgrade can replace it with their real marketing origin.
Why is Acquisition Channel "Direct" for a contact that came from our website?
"Direct" means we couldn't see a marketing origin — they typed your URL, used a
bookmark, or arrived through an internal link tagged utm_source=website. It's
the honest answer when no external source is known.
Can I set attribution myself? Yes — include acquisition fields in your CSV import or API call. Values you supply explicitly are honored, classified, and then locked so automatic attribution never overwrites them.
Why didn't Latest Source change when the contact visited our site directly? By design: a direct visit doesn't erase a known marketing source. The latest-touch timestamp still updates.
Which of our WhatsApp numbers did this contact come through? Record Source Detail records the specific integration (number) that received the first message, so multi-number teams can segment by line.
A WhatsApp contact shows Channel = Paid Social. Is that a bug? No — that's the system working correctly. They started the chat by tapping a click-to-WhatsApp ad, so the record was created via WhatsApp but the person was acquired through a paid social ad.
What timezone are the attribution dates in? All attribution timestamps are stored in UTC and displayed using your organization's timezone settings.
What about visitors who haven't become contacts yet? Anonymous visitor activity is tracked separately — see Visitor Tracking. The moment a visitor identifies (form fill, chat identification), their entire anonymous history is linked to the new contact and drives its first-touch attribution.