How to Store UTM Parameters in CRM Hidden Fields (Salesforce, HubSpot & Forms)

Complete guide on capturing UTM parameters in hidden form fields and syncing attribution into Salesforce, HubSpot, and Zoho CRM without losing data.

Technical diagram showing incoming UTM query parameters persisting in browser cookies and auto-populating CRM form fields
Table of Contents
  1. Why Web Forms Need Hidden Attribution Fields
  2. Setting Up Custom Properties in HubSpot and Salesforce
  3. HTML Form Markup for Hidden Parameters
  4. The JavaScript Autofill Script (Handles Direct & Multi-Page Navigation)
  5. Testing Form Submission & CRM Ingestion

Add hidden fields to your lead forms if you need campaign information alongside contact details in your CRM. You will also need a way to keep those values as visitors move between pages. The examples below show how to capture, store and submit them.

Why Web Forms Need Hidden Attribution Fields

Google Analytics 4 tracks aggregate session counts, but it cannot legally tell your sales reps which specific marketing touchpoint convinced a specific company to request a demo. Capturing UTM parameters directly into your CRM Contact record connects individual ad clicks to pipeline value, deal size, and closed-won revenue.

Setting Up Custom Properties in HubSpot and Salesforce

Before modifying web forms, create matching custom contact fields in your CRM:

  • utm_source (Single-line text)
  • utm_medium (Single-line text)
  • utm_campaign (Single-line text)
  • utm_content (Single-line text)
  • utm_term (Single-line text)
  • gclid (Single-line text)

HTML Form Markup for Hidden Parameters

Add hidden inputs inside your HTML <form> element:

<form id="lead-form" action="/submit" method="POST">
  <!-- Visible Form Fields -->
  <input type="text" name="first_name" placeholder="First Name" required>
  <input type="email" name="email" placeholder="Work Email" required>

  <!-- Hidden UTM Attribution Fields -->
  <input type="hidden" name="utm_source" id="utm_source">
  <input type="hidden" name="utm_medium" id="utm_medium">
  <input type="hidden" name="utm_campaign" id="utm_campaign">
  <input type="hidden" name="utm_term" id="utm_term">
  <input type="hidden" name="utm_content" id="utm_content">
  <input type="hidden" name="gclid" id="gclid">

  <button type="submit">Request Demo</button>
</form>

The JavaScript Autofill Script (Handles Direct & Multi-Page Navigation)

This illustrative function saves a complete latest tagged campaign bundle and fills matching hidden fields. Connect it to your consent manager and form-ready lifecycle. Host-only cookies do not share values across subdomains; adapt that intentionally if needed. The script does not validate submitted values or perform GA4 attribution, so verify fields on the receiving server and inspect a test lead:

// Call from your consent manager only when storage is permitted.
function captureCampaign(allowed) {
  if (!allowed || location.protocol !== 'https:') return;
  const keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'gclid'];
  const url = new URLSearchParams(location.search);
  let campaign = null;
  try {
    const raw = document.cookie.split('; ').find(value => value.startsWith('campaign_context='));
    if (raw) campaign = JSON.parse(decodeURIComponent(raw.slice('campaign_context='.length)));
  } catch { campaign = null; }
  if (url.get('utm_source') || url.get('gclid')) {
    // Replace the whole bundle; missing values clear old campaign fields.
    campaign = Object.fromEntries(keys.map(key => [key, url.get(key) || '']));
    campaign.captured_at = new Date().toISOString();
    document.cookie = 'campaign_context=' + encodeURIComponent(JSON.stringify(campaign))
      + '; Max-Age=2592000; Path=/; SameSite=Lax; Secure';
  }
  function fill() {
    if (!campaign) return;
    keys.forEach(key => {
      document.querySelectorAll('input[type="hidden"][name="' + key + '"]').forEach(input => {
        input.value = typeof campaign[key] === 'string' ? campaign[key] : '';
      });
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', fill, { once: true });
  } else { fill(); }
  // For forms inserted later, call captureCampaign again from the form's ready hook.
}
// Example integration: call captureCampaign(true) in your permitted-consent callback.
// When consent is withdrawn, clear campaign_context and the populated hidden fields.

Testing Form Submission & CRM Ingestion

  1. Open an Incognito browser window.
  2. Paste your test URL: https://example.com/?utm_source=qa_test&utm_medium=paid_social&utm_campaign=demo_audit.
  3. Click through to your contact or pricing form page.
  4. Right-click the form, click Inspect, and verify that the <input type="hidden"> elements contain the expected values in their current value properties.
  5. Submit a test lead and verify that the contact record inside HubSpot or Salesforce displays the parameters accurately.

For the broader setup, read UTM Operations: How to Build a Reliable Campaign Tracking Workflow. Browse the UTM Operations, Automation & QA guides to choose another troubleshooting task.

References