Published by Terminus, the marketing taxonomy governance platform.
Salesforce UTM Capture and Lead Source Attribution: A Complete 2026 Guide
Last updated
Capturing UTM parameters into Salesforce is the difference between a CRM that tells you which campaigns generated pipeline and one that quietly buckets seventy percent of your inbound into a generic “Web” lead source. The mechanics are not hard. The discipline is. This guide is for marketing operations and RevOps practitioners running B2B SaaS go-to-market on Salesforce, and it covers the full path: where UTMs enter your forms, how to persist them across sessions, how to write them onto Lead and Contact records, how to design a Lead Source field that survives a quarter, and how to report on marketing-sourced pipeline without lying to yourself.
Published by Terminus, the marketing taxonomy governance platform.
TL;DR
- Salesforce will not capture UTMs for you. You add hidden form fields, write a JavaScript layer that reads parameters and cookies, and map those fields to custom Lead and Contact properties.
- Most B2B SaaS teams want first-touch UTMs (preserved forever) and last-touch UTMs (overwritten on each session). Store them in separate field sets.
- The standard Lead Source picklist is too coarse to drive marketing reporting. Pair it with custom UTM fields and a derived Channel field driven by automation.
- Pardot, now branded Marketing Cloud Account Engagement, has its own capture quirks: iframe forms, prospect cookies, and the
pi_parameter family.- Marketing-sourced pipeline reporting only works if dedup and lead-to-contact conversion preserve the first-touch UTM set. Most teams break this on the convert step.
The capture-side problem: where UTMs come into Salesforce
Salesforce has no native UTM capture. The Lead object has a single LeadSource picklist and that is the entire out-of-the-box attribution story. UTMs enter Salesforce through one of four paths:
Web-to-Lead. A native Salesforce feature that posts an HTML form to https://webto.salesforce.com/servlet/servlet.WebToLead. The form must include hidden inputs for every UTM field you want to capture. JavaScript on the page populates those inputs before submission.
Pardot / Marketing Cloud Account Engagement forms. Pardot was renamed Marketing Cloud Account Engagement in April 2022 but most practitioners still call it Pardot. Forms come in two flavors: native Pardot forms (rendered in an iframe) and Pardot form handlers (you build the form in your own HTML, post it to a Pardot endpoint). Form handlers are vastly more flexible for UTM capture because you control the DOM.
Marketo, HubSpot, or other marketing automation forms that sync to Salesforce. Many B2B SaaS teams keep Marketo or HubSpot as the source-of-truth for forms and pipe records into Salesforce through a sync. UTM capture happens in the marketing automation layer, and the sync maps custom fields to Salesforce custom fields.
Custom-built forms posting through Apex REST or the Salesforce REST API. You write your own form, persist UTMs yourself, and post a JSON payload to a custom Apex REST endpoint. This is what most B2B SaaS teams above $20M ARR end up doing because Web-to-Lead breaks down once you need server-side validation, deduplication, or progressive profiling.
Each path has the same conceptual shape: read UTMs from the URL or a cookie, write them into form fields, post the form, map the form fields to custom Salesforce fields. They differ in how much plumbing you build versus inherit.
First-touch vs last-touch capture: why most teams want both
A prospect visits your site three times. First visit comes from a Google paid ad with utm_source=google&utm_medium=cpc&utm_campaign=brand-2026q2. Second visit is direct. Third visit comes from a LinkedIn post with utm_source=linkedin&utm_medium=social. They fill out a demo form on the third visit.
Which campaign gets credit? First-touch credits the Google paid ad: the campaign that originally introduced them. Last-touch credits the LinkedIn post: the campaign that closed the loop. Both are partial truths, and that is why most B2B SaaS attribution setups store both.
In Salesforce terms, that means two parallel sets of custom UTM fields on Lead and Contact:
FT_UTM_Source__c,FT_UTM_Medium__c,FT_UTM_Campaign__c,FT_UTM_Content__c,FT_UTM_Term__c,FT_Landing_Page__c,FT_Timestamp__cLT_UTM_Source__c,LT_UTM_Medium__c,LT_UTM_Campaign__c,LT_UTM_Content__c,LT_UTM_Term__c,LT_Landing_Page__c,LT_Timestamp__c
The capture rule: first-touch fields are written once and never overwritten. Last-touch fields are overwritten on every new session where UTMs are present. The “never overwritten” rule must be enforced both in the JavaScript layer (don’t overwrite the first-touch cookie) and on the Salesforce side (don’t let a workflow blow away first-touch on lead conversion).
For multi-touch attribution beyond two points, capture every session as a touchpoint on a related custom object (a “Campaign Influence” pattern) or push session data into a warehouse. The two-field-set pattern gets most B2B SaaS teams to honest reporting.
Cookie persistence strategy
UTM parameters only live in the URL on the landing-page hit. By the time the prospect submits a form three pages later, window.location.search is empty. You need a persistence layer. There are three places it can live.
Salesforce-native (Web-to-Lead with JS). A JavaScript snippet on every page reads window.location.search, writes parameters to first-party cookies, and on form submit reads the cookies into hidden inputs. The form posts directly to Salesforce. Works, but fragile: the snippet lives in your CMS and breaks when someone forgets it on a new landing page template.
Marketing automation cookies (Pardot, Marketo, HubSpot). Pardot’s visitor_id, Marketo’s _mkto_trk, and HubSpot’s hubspotutk all persist visitor identity, and each platform offers ways to attach UTM parameters to the visitor record. The catch: each platform stores UTMs differently. Pardot writes them to the Prospect, but only if present at form-fill time. Marketo surfaces them through Acquisition Program. HubSpot exposes Original Source plus optional custom properties. If you want consistent first-touch and last-touch across the funnel, do not rely on platform defaults alone.
Custom JS layer (recommended for B2B SaaS at scale). A small first-party JavaScript module, loaded on every page, that owns persistence end to end:
(function () {
var UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
var COOKIE_DAYS_FT = 365;
var COOKIE_DAYS_LT = 30;
function getParam(name) {
var match = window.location.search.match(new RegExp('[?&]' + name + '=([^&]+)'));
return match ? decodeURIComponent(match[1].replace(/\+/g, ' ')) : null;
}
function setCookie(name, value, days) {
var d = new Date();
d.setTime(d.getTime() + days * 864e5);
document.cookie = name + '=' + encodeURIComponent(value) +
';path=/;expires=' + d.toUTCString() + ';SameSite=Lax';
}
function getCookie(name) {
var m = document.cookie.match(new RegExp('(^|;)\\s*' + name + '=([^;]+)'));
return m ? decodeURIComponent(m[2]) : null;
}
var hasUtmsInUrl = UTM_KEYS.some(function (k) { return getParam(k) !== null; });
if (hasUtmsInUrl) {
UTM_KEYS.forEach(function (key) {
var value = getParam(key);
if (value) {
// First-touch: write only if not already set
if (!getCookie('ft_' + key)) {
setCookie('ft_' + key, value, COOKIE_DAYS_FT);
}
// Last-touch: always overwrite
setCookie('lt_' + key, value, COOKIE_DAYS_LT);
}
});
// Timestamps and landing page
if (!getCookie('ft_landing_page')) {
setCookie('ft_landing_page', window.location.pathname, COOKIE_DAYS_FT);
setCookie('ft_timestamp', new Date().toISOString(), COOKIE_DAYS_FT);
}
setCookie('lt_landing_page', window.location.pathname, COOKIE_DAYS_LT);
setCookie('lt_timestamp', new Date().toISOString(), COOKIE_DAYS_LT);
}
// Expose a helper that forms can call before submission
window.TerminusAttribution = {
populate: function (form) {
UTM_KEYS.forEach(function (key) {
var ftField = form.querySelector('[name="ft_' + key + '"]');
var ltField = form.querySelector('[name="lt_' + key + '"]');
if (ftField) ftField.value = getCookie('ft_' + key) || '';
if (ltField) ltField.value = getCookie('lt_' + key) || '';
});
}
};
})();
A few details that matter in practice. SameSite=Lax is the right default for first-party context; if your form is iframed from a different domain (Pardot iframe forms) you need SameSite=None; Secure. The 365-day first-touch lifetime is one common choice but is contentious under PECR and UK GDPR for EU and UK visitors, where regulators expect cookie lifetimes “no longer than necessary”; many teams shorten to 90 to 180 days for those regions. The pathname-only landing page avoids leaking session-IDs or auth tokens that might appear in query strings. If your marketing site and form host live on different subdomains (www.example.com and go.example.com), the snippet above sets host-only cookies that will not be readable across subdomains; add ;domain=.example.com to both setCookie calls when you need that cross-subdomain access. Keep the leading dot intentional and never set domain= on cookies you do not own.
This JS layer is the single source of truth. Every form calls TerminusAttribution.populate(form) before submission. Change your capture strategy in one file.
Apex sample: accepting UTMs in a custom controller
When you outgrow Web-to-Lead and want server-side validation, dedup, or progressive profiling, the standard path is an Apex REST resource. The front-end form posts JSON, the controller validates and upserts, and you control every business rule. A minimal example that dedups by email and either updates an existing record’s last-touch fields or creates a new Lead with both first-touch and last-touch sets is below.
Prerequisites before deploying this class. The sample references custom fields (FT_UTM_Source__c, LT_UTM_Source__c, and so on, plus FT_Landing_Page__c, LT_Landing_Page__c, FT_Timestamp__c, LT_Timestamp__c) on both Lead and Contact. Create those fields in your org first or the class will not compile. The recommended Lead Source restricted picklist values are listed later in this article (“Lead Source field strategy”). The mapping function below only returns values that exist in that picklist; if you change the picklist values, update the function in lockstep.
@RestResource(urlMapping='/lead-capture/*')
global with sharing class LeadCaptureController {
global class LeadPayload {
public String email;
public String firstName;
public String lastName;
public String company;
public String ftUtmSource;
public String ftUtmMedium;
public String ftUtmCampaign;
public String ftUtmContent;
public String ftUtmTerm;
public String ftLandingPage;
public String ltUtmSource;
public String ltUtmMedium;
public String ltUtmCampaign;
public String ltUtmContent;
public String ltUtmTerm;
public String ltLandingPage;
}
@HttpPost
global static Map<String, Object> captureLead(LeadPayload payload) {
if (String.isBlank(payload.email)) {
RestContext.response.statusCode = 400;
return new Map<String, Object>{ 'error' => 'email is required' };
}
try {
// Look for an existing Contact first; Contacts win over Leads on dedup
List<Contact> existingContacts = [
SELECT Id, AccountId
FROM Contact
WHERE Email = :payload.email
LIMIT 1
];
if (!existingContacts.isEmpty()) {
Contact c = existingContacts[0];
c.LT_UTM_Source__c = payload.ltUtmSource;
c.LT_UTM_Medium__c = payload.ltUtmMedium;
c.LT_UTM_Campaign__c = payload.ltUtmCampaign;
c.LT_UTM_Content__c = payload.ltUtmContent;
c.LT_UTM_Term__c = payload.ltUtmTerm;
c.LT_Landing_Page__c = payload.ltLandingPage;
c.LT_Timestamp__c = System.now();
update c;
return new Map<String, Object>{
'status' => 'contact_updated',
'id' => c.Id
};
}
List<Lead> existingLeads = [
SELECT Id, FT_UTM_Source__c
FROM Lead
WHERE Email = :payload.email AND IsConverted = false
LIMIT 1
];
if (!existingLeads.isEmpty()) {
Lead l = existingLeads[0];
// Never overwrite first-touch; always overwrite last-touch
l.LT_UTM_Source__c = payload.ltUtmSource;
l.LT_UTM_Medium__c = payload.ltUtmMedium;
l.LT_UTM_Campaign__c = payload.ltUtmCampaign;
l.LT_UTM_Content__c = payload.ltUtmContent;
l.LT_UTM_Term__c = payload.ltUtmTerm;
l.LT_Landing_Page__c = payload.ltLandingPage;
l.LT_Timestamp__c = System.now();
update l;
return new Map<String, Object>{
'status' => 'lead_updated',
'id' => l.Id
};
}
Lead newLead = new Lead(
Email = payload.email,
FirstName = payload.firstName,
LastName = String.isBlank(payload.lastName) ? 'Unknown' : payload.lastName,
Company = String.isBlank(payload.company) ? 'Unknown' : payload.company,
LeadSource = deriveLeadSource(payload.ftUtmMedium, payload.ftUtmSource),
FT_UTM_Source__c = payload.ftUtmSource,
FT_UTM_Medium__c = payload.ftUtmMedium,
FT_UTM_Campaign__c = payload.ftUtmCampaign,
FT_UTM_Content__c = payload.ftUtmContent,
FT_UTM_Term__c = payload.ftUtmTerm,
FT_Landing_Page__c = payload.ftLandingPage,
FT_Timestamp__c = System.now(),
LT_UTM_Source__c = payload.ltUtmSource,
LT_UTM_Medium__c = payload.ltUtmMedium,
LT_UTM_Campaign__c = payload.ltUtmCampaign,
LT_UTM_Content__c = payload.ltUtmContent,
LT_UTM_Term__c = payload.ltUtmTerm,
LT_Landing_Page__c = payload.ltLandingPage,
LT_Timestamp__c = System.now()
);
insert newLead;
return new Map<String, Object>{
'status' => 'lead_created',
'id' => newLead.Id
};
} catch (DmlException e) {
RestContext.response.statusCode = 500;
return new Map<String, Object>{
'error' => 'persistence_failed',
'message' => e.getMessage()
};
} catch (Exception e) {
RestContext.response.statusCode = 500;
return new Map<String, Object>{
'error' => 'internal_error',
'message' => e.getMessage()
};
}
}
// Maps utm_medium to a value from the recommended restricted Lead Source picklist:
// Paid Search, Organic Search, Paid Social, Organic Social, Email, Referral,
// Direct, Event, Partner, Outbound, Other. Anything that does not match a known
// medium falls back to 'Other' so insert never trips the restricted picklist.
private static String deriveLeadSource(String medium, String source) {
if (String.isBlank(medium)) return 'Other';
String m = medium.toLowerCase();
if (m == 'cpc' || m == 'ppc' || m == 'paidsearch') return 'Paid Search';
if (m == 'organic') return 'Organic Search';
if (m == 'paid-social' || m == 'paidsocial' || m == 'cpm') return 'Paid Social';
if (m == 'social' || m == 'organic-social' || m == 'organicsocial') return 'Organic Social';
if (m == 'email') return 'Email';
if (m == 'referral') return 'Referral';
return 'Other';
}
}
Returning Map<String, Object> instead of a raw JSON string lets the Salesforce REST framework serialize the response. Returning a String double-encodes the body (Salesforce wraps the String in quotes and escapes the inner JSON), which breaks most clients. The try / catch block converts a DML failure (validation rule, required field, duplicate rule) into a structured 500 instead of a raw stack.
The dedup strategy prefers Contacts over Leads: once a prospect is converted, subsequent form fills should update the Contact, not create a new Lead in a different funnel. LeadSource is derived from utm_medium through a small mapping function (covered in the next section). LastName and Company default to 'Unknown' because Salesforce requires both on Lead insert; in production, surface a validation error to the form instead. In production code, add a @TestVisible annotation on the helper and write a full test class hitting RestContext with mocked RestRequest.
Lead Source field strategy
Salesforce’s standard Lead Source picklist values are: Web, Phone Inquiry, Partner Referral, Purchased List, Other. They are a relic. Every B2B SaaS team customizes this picklist within a month of go-live, and that customization usually goes badly.
Two failure modes show up over and over:
Drift. Reps add free-text values. Managers add new picklist values without consulting marketing ops. Within a year the picklist has eighty entries: “Webinar”, “webinar”, “Webinar - Q3”, “Webinar (live)”, “WEBINAR”, “Live Webinar”, “Webinar Q4 2025”. Channel-grouping reports become unusable.
Conflation. Lead Source is asked to be three things at once: a marketing channel (Paid Search), a lead-gen mechanism (Inbound Demo Request), and an originating campaign (Salesforce-Connections-2025). When one field carries three meanings, downstream reporting picks whichever meaning the report author guessed.
The fix is a small controlled vocabulary, automation-driven, with separate fields for the other meanings.
Recommended structure:
LeadSource(standard, picklist): channel category only. Restricted picklist with eight to twelve values:Paid Search,Organic Search,Paid Social,Organic Social,Email,Referral,Direct,Event,Partner,Outbound,Other. Set as a restricted picklist so reps and Apex cannot insert ad-hoc values.Lead_Source_Detail__c(custom, text): captures the originating campaign or mechanism. Set fromFT_UTM_Campaign__cby automation.FT_UTM_Source__c,FT_UTM_Medium__cetc. as above: the raw, unprocessed first-touch context.
The automation: on Lead insert, if LeadSource is blank, derive it from FT_UTM_Medium__c using the same mapping shown in the Apex controller above. If a record is created without UTMs (a rep dialed an outbound prospect), the rep sets LeadSource manually and the UTM fields stay null. The two pathways coexist cleanly.
Worked example: governing the UTM values that feed the picklist
The Lead Source picklist only stays clean if the upstream UTMs are clean. If marketers tag campaigns with utm_medium=ppc one week and utm_medium=cpc the next, your “Paid Search” attribution will be wrong by the difference.
This is the problem Terminus solves on the upstream side. Marketers build campaign URLs in Terminus rather than free-typing them into spreadsheets. The utm_medium field is a controlled picklist (cpc, organic, social, email, display, referral) defined once and enforced everywhere. The utm_source picklist is governed by team. The utm_campaign field follows a naming convention validated by regex.
When Salesforce receives the captured UTMs, the values are already in the controlled vocabulary. The deriveLeadSource function maps a small known set of utm_medium values to a small known set of LeadSource channel categories, and there is no drift. The operational case is not “we need a tool to build URLs” but “we need an upstream constraint so the downstream picklist does not rot”. The value compounds the larger the marketing team gets.
Reporting: marketing-sourced pipeline, channel attribution, leakage diagnostics
Once UTMs and Lead Source are captured cleanly, the reports you want are not exotic.
Marketing-sourced pipeline by first-touch channel. Report Type “Opportunities with Contact Roles”. Group by Contact.FT_UTM_Medium__c (or the derived LeadSource) and sum Amount for opportunities in Stage 2 and above with CreatedDate in the last quarter. This is your “first-touch marketing-sourced pipeline” number.
Channel attribution comparison. Two reports side by side: one grouped by FT_UTM_Medium__c, one by LT_UTM_Medium__c. The difference shows where first-touch and last-touch disagree. Paid search often has higher last-touch share (people search for you after seeing a paid social ad). Organic content often shows higher first-touch share (people read your blog months before converting on branded search). Both are real.
Leakage diagnostics. A report on Lead and Contact records created in the last 90 days where FT_UTM_Source__c is null but LeadSource = 'Web'. These are records where capture failed: the snippet did not run, cookies expired, parameters were stripped. A leakage rate above ten percent of inbound web leads means you have a coverage problem worth fixing before trusting any channel attribution.
Lightning Report Builder lives at App Launcher → Reports → New Report. The “Reports & Dashboards” Trailhead module is the current authoritative walkthrough.
The Pardot / Marketing Cloud Account Engagement path
Pardot has its own quirks worth calling out, because the product is widely deployed in B2B SaaS and the documentation can be confusing thanks to the 2022 rebrand.
Naming. Pardot is officially Marketing Cloud Account Engagement (MCAE) as of April 2022. Salesforce’s developer docs and the AppExchange use the new name; practitioners and most third-party content still use Pardot. Both names refer to the same product. This guide uses “Pardot” for brevity.
Form flavors. Pardot offers native Pardot forms (rendered via iframe) and form handlers (you build the HTML, you control the DOM, you POST to a Pardot endpoint that creates or updates the Prospect). For UTM capture, form handlers are strictly superior. You can run your TerminusAttribution.populate JS layer on a form handler the same way you do on any custom form. With iframe forms, parameters have to be passed through the iframe src URL using Pardot’s pi_ prefix convention, which is brittle.
Prospect cookie. Pardot’s tracking script (piAId and piCId cookies) identifies returning visitors. When a known Prospect submits a form, Pardot updates the existing Prospect record rather than creating a duplicate. This is helpful for last-touch updates but means you must explicitly write to first-touch fields with “set if blank” automation to avoid clobbering them.
Salesforce sync. Pardot Prospects sync to Salesforce Leads (and Contacts if matching is enabled). The custom UTM fields you create on Pardot Prospect must be mapped to corresponding custom fields on Salesforce Lead and Contact through the Sync Behavior settings. In the Account Engagement Lightning app, navigate to Account Engagement Settings → Connectors → Salesforce connector row → gear menu → Edit Settings. If you forget to map the fields, the UTMs stay on the Prospect record and never reach Salesforce.
Pardot URL parameters with the pi_ prefix. Pardot’s documentation references parameters like pi_campaign_id, used for native Pardot campaign attribution. These are separate from your custom UTM fields and from the standard utm_* parameters. Do not conflate them. Keep utm_* for your channel attribution, use pi_campaign_id only if you are actively driving Pardot campaign IDs through links.
Edge cases: cross-domain forms, anonymous visitors, GDPR-region forms
Cross-domain submissions. Your marketing site lives at www.example.com and your demo form is hosted on a Pardot iframe at go.pardot.com. First-party cookies set on www.example.com are not readable on go.pardot.com. The fix: persist UTMs in sessionStorage on the parent page, then post them through window.postMessage to the iframe, or pass them as URL parameters in the iframe src. The form handler approach avoids the problem entirely.
Anonymous visitors who never convert. Most of your inbound traffic never fills a form. Salesforce only sees the records that submitted. This is fine for CRM attribution (you cannot attribute pipeline to a non-prospect), but be careful when reporting “channel performance” out of Salesforce: those numbers describe converted-prospect distribution, not visitor distribution. Pair Salesforce reporting with GA4 or your warehouse for the upstream funnel.
GDPR-region forms and consent. In the EU and the UK, first-party cookies used for marketing attribution require user consent under PECR and the UK GDPR. The clean pattern is: load your TerminusAttribution script only after the consent banner has been accepted. Cookieless visitors get null UTM fields on submission. Your reporting will under-represent EU and UK channel performance; that is correct behavior, not a bug. Do not “fix” it by setting cookies before consent.
iOS Link Tracking Protection. Apple’s Link Tracking Protection (introduced in iOS 17, expanded through iOS 17.4 and 18) strips known tracking parameters from URLs in Mail, Messages, and Safari Private Browsing. The community-sourced strip list (from PrivacyTests.org and similar) reports that utm_* is generally not stripped, but several click ID parameters are. Confirm against current docs and the PrivacyTests.org list before relying on a specific parameter surviving.
Form spam. Spam fills will pollute your UTM data with garbage referrers and made-up campaigns. Add a honeypot field to your forms, validate referrer URLs server-side in your Apex controller, and consider reCAPTCHA Enterprise for high-value forms. Spam UTMs not only mess up reports, they also burn through Salesforce data storage if you do not have automated cleanup.
Field length limits. Salesforce text fields default to 255 characters. A long utm_content or referrer URL can hit the limit. Either truncate on the client (value.slice(0, 255)) or use Long Text Area fields. Truncating on the client is simpler.
FAQ
How do I capture UTMs in Salesforce Web-to-Lead?
Generate the Web-to-Lead HTML from Setup → Quick Find → “Web-to-Lead” → Create Web-to-Lead Form (Web-to-Lead lives under Feature Settings → Marketing in the Setup tree; the Quick Find search is the reliable way to navigate to it across Lightning UI versions). Select the custom UTM fields you have already created on the Lead object, and copy the generated form HTML to your site. Add a JavaScript snippet on every page that reads UTM parameters from the URL into first-party cookies, and on form submit reads those cookies into the hidden form inputs. The form posts directly to webto.salesforce.com and creates the Lead with the UTM fields populated.
What is the Salesforce Lead Source field?
Lead Source (LeadSource on the Lead object, also present on Contact and Opportunity) is a standard picklist field that captures the originating channel for a record. Default values are Web, Phone Inquiry, Partner Referral, Purchased List, and Other. Most B2B SaaS teams customize the picklist to track marketing channels and use automation to set it from captured UTM parameters. It is editable from Setup → Object Manager → Lead → Fields & Relationships → Lead Source.
Can Salesforce capture first-touch and last-touch UTMs separately?
Not out of the box. You create two parallel sets of custom fields (FT_UTM_Source__c, LT_UTM_Source__c, and so on), and your JavaScript persistence layer plus form handler logic enforces the rule that first-touch fields are written only once while last-touch fields are overwritten on every new session. Both sets sync to Salesforce through your form submission path.
What is the difference between Pardot and Marketing Cloud Account Engagement?
They are the same product. Pardot was renamed Marketing Cloud Account Engagement (MCAE) in April 2022. Salesforce’s developer docs and the AppExchange use the new name; practitioners and most published content still use “Pardot”. When searching documentation, both names work, but Salesforce’s own URLs and product pages favor “Account Engagement”.
How do I prevent UTM data from being lost on lead conversion?
On lead conversion, Salesforce copies field values from Lead to Contact, Account, and Opportunity according to the lead field mapping configured under Setup → Object Manager → Lead → Fields & Relationships → Map Lead Fields (the Map Lead Fields button sits at the top of the Fields & Relationships page, not as a separate menu node). Map all your custom UTM fields (both first-touch and last-touch) to corresponding custom fields on Contact. The fields must exist on Contact first. If you do not configure the mapping, the UTM data dies on the Lead record once the lead is converted, and your downstream pipeline reports cannot trace back to first-touch.
Why is my Lead Source picklist filled with duplicate values?
Because users (or Apex, or sync integrations) inserted values that were not on the original picklist. The fix is to set Lead Source as a Restricted Picklist under Setup → Object Manager → Lead → Fields & Relationships → Lead Source → Edit, which prevents any value not on the picklist from being saved. Then run a one-time cleanup with a Data Loader CSV export and bulk update to map the duplicates to canonical values. Going forward, enforce the controlled vocabulary upstream in your UTM tagging tool so the captured utm_medium values are themselves controlled.
How do I report on marketing-sourced pipeline in Salesforce?
Build an Opportunities report with the Report Type “Opportunities with Contact Roles” or “Opportunities with Primary Contact Role”. Filter to opportunities at Stage 2 or higher with a CreatedDate in the period you care about. Group by Contact.FT_UTM_Medium__c or Contact.LeadSource, summarize on Amount. Save the report and add it to a Marketing Performance dashboard. This is the standard first-touch marketing-sourced pipeline report.
What is the difference between Web-to-Lead and a custom Apex form endpoint?
Web-to-Lead is a built-in Salesforce feature where forms post directly to webto.salesforce.com and create Lead records. It is fast to set up and requires no Apex. A custom Apex REST endpoint is something you build yourself: a class annotated with @RestResource that accepts JSON, runs custom validation and dedup logic, and creates or updates records through SOQL and DML. Custom endpoints are more flexible but require Apex development and unit tests. Most B2B SaaS teams start with Web-to-Lead and migrate to a custom endpoint once they need progressive profiling, server-side validation, or anti-spam logic.
Can I capture UTMs in HubSpot and sync them into Salesforce?
Yes. Set up custom contact properties in HubSpot for first-touch and last-touch UTMs, configure HubSpot’s tracking code or a custom snippet to populate them on form submission, and configure the HubSpot-Salesforce sync (under HubSpot Settings → Integrations → Salesforce → Contact sync) to map the HubSpot properties to your custom Salesforce Contact and Lead fields. HubSpot’s hubspotutk cookie handles its own visitor identity; your custom JS still handles UTM persistence.
How do I keep the UTM values consistent so my Lead Source automation works?
By governing UTM tagging upstream. The deriveLeadSource mapping in your Apex controller only works if utm_medium arrives with values it recognizes. If marketers free-type ppc one week and cpc the next and paid-search the week after, the mapping breaks. The standard pattern for mid-to-large marketing teams is a UTM management platform that enforces a controlled vocabulary at link-build time. Terminus is one option; Claravine, Accutics, and CampaignTrackly are others. The key is upstream constraint, not downstream cleanup.
Last updated: 2026-06-23.
- 3 Users
- 5 Projects
- 2 Custom Domains
- Simple Taxonomy
- UTM Rules
- Presets
- Labels
- Notes
- Custom Parameters
- Multi-tag UTM Builder
- Auto-shortening
- Click Reports
- Fine-grained User Permissions
- Auditing Tools
- Chrome Extension
- Custom Domain SSL
- URL Monitoring
- Redirect Codes / Link Retargeting
- Bulk Operations
- 5 Users
- 10 Projects
- 3 Custom Domains
- All Taxonomy Types
- Bulk URL Cloning
- QR Codes
- Conventions
- Grid Mode URL Builder
- Email Builder
- Auto-generated Tracking IDs
- Automated Exports
- API Access
- Custom Users
- Custom Projects
- Custom Domains
- Single Sign-On (SSO)
- Invoice Billing
- Signed Agreement
- SOC 2 Type 2