A visitor arrives through a campaign link, browses two service pages, and submits your contact form.

The enquiry reaches your CRM. The campaign information does not.

The sales team sees a new lead, but nobody can confidently explain which campaign generated it.

A common implementation gap causes this: the website reads UTM parameters only on the page where the form is submitted. By that point, the visitor has moved away from the original tagged URL.

This article walks through a small, framework-independent pattern for preserving campaign information during same-tab, same-origin navigation.

The code is illustrative. Adapt it to your form implementation and test it before deployment.

1. Define what you want to preserve

Consider this campaign URL:

https://example.com/crm-development ?utm_source=linkedin &utm_medium=paid_social &utm_campaign=crm_demo &utm_content=workflow_ad 

The parameters describe different campaign attributes:

Parameter Example Purpose
utm_source linkedin Identifies the traffic source
utm_medium paid_social Identifies the marketing medium
utm_campaign crm_demo Identifies the campaign
utm_content workflow_ad Distinguishes creative or link variations
utm_term crm_integration Carries a keyword or targeting label when used

Use a shared naming convention. Otherwise, values such as LinkedIn, linkedin, and linked-in can make reporting unnecessarily fragmented.

Do not put names, email addresses, phone numbers, or enquiry text into campaign parameters.

2. Choose an attribution rule before writing code

“Save the UTMs” leaves an important question unanswered: what happens when another tagged visit occurs?

For this example, we will use this rule:

Preserve the first tagged campaign encountered during the current browser tab’s page session.

That means:

  • A tagged landing page creates the stored record.
  • An untagged internal page preserves it.
  • A later tagged URL in that page session does not overwrite it.
  • An untagged visit without a stored record remains unattributed.

This is a limited capture rule. It does not identify a person’s first-ever interaction, and it does not reproduce GA4 attribution.

If your business needs the latest tagged campaign instead, implement that deliberately and name the CRM fields accordingly.

3. Capture campaign parameters on landing

Run the capture logic on every relevant landing page, before a visitor navigates away.

URLSearchParams handles reading the query string:

const UTM_KEYS = [ "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term" ]; const STORAGE_KEY = "lead_campaign_v1"; function readCampaign() { try { const raw = sessionStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function captureCampaign() { const existing = readCampaign(); if (existing) { return existing; } const params = new URLSearchParams( window.location.search ); const hasCampaign = UTM_KEYS.some( (key) => Boolean(params.get(key)?.trim()) ); if (!hasCampaign) { return null; } const campaign = { captured_at: new Date().toISOString(), landing_path: window.location.pathname }; for (const key of UTM_KEYS) { campaign[key] = (params.get(key) || "") .trim() .slice(0, 200); } try { sessionStorage.setItem( STORAGE_KEY, JSON.stringify(campaign) ); } catch { // Continue without persistent campaign storage. } return campaign; } const campaignForPage = captureCampaign(); 

This stores selected fields rather than copying the entire URL, which may contain unrelated or sensitive query parameters.

The length limit is a basic payload constraint. It does not make the values trustworthy.

Also, this example assumes your landing paths contain no personal information. Use an allowlisted page identifier if that assumption does not hold.

4. Understand the storage boundary

sessionStorage is scoped to the origin and browser tab’s page session. It survives ordinary reloads but is cleared when that page session ends.

Consequently, this implementation does not provide:

  • Persistent attribution across future visits.
  • Automatic sharing between different subdomains.
  • Reliable continuity across separate tabs.
  • Automatic transfer into an externally hosted form.

For example, www.example.com and forms.example.com have different origins.

If your enquiry journey crosses those boundaries, design an explicit handoff. Do not assume the stored values will follow the visitor.

Run campaign storage through your site’s consent controls where required. If storage is unavailable, the form should still work.

5. Attach the campaign to the form request

If your form already submits using FormData, add campaign fields when constructing the request:

const form = document.querySelector("#contact-form"); function buildLeadPayload(form) { const payload = new FormData(form); const campaign = readCampaign() || campaignForPage; for (const key of UTM_KEYS) { payload.set(key, campaign?.[key] || ""); } payload.set( "campaign_landing_path", campaign?.landing_path || "" ); payload.set( "campaign_captured_at", campaign?.captured_at || "" ); return payload; } // Inside your existing submission handler: const payload = buildLeadPayload(form); // Send payload using your existing request logic. 

Keep this inside your existing submission flow. Adding another submit listener that independently sends the form can create duplicate enquiries.

For native HTML form submissions, populate named hidden inputs instead. The backend mapping is still necessary.

6. Validate and map the fields on the backend

Sending a field does not guarantee that it reaches the CRM.

Check each stage:

Stage What to verify
Browser request Campaign fields appear in the submitted payload
Backend Only expected fields are accepted and validated
CRM integration Each field maps to the intended CRM property
CRM record Values are saved and visible in reporting

Treat browser-supplied attribution as untrusted metadata. Visitors can edit query strings and submitted fields.

Apply server-side type and length checks, and escape values when displaying them. Never use campaign parameters to determine access permissions or other sensitive decisions.

For existing contacts, define an update rule too. A repeat enquiry should not accidentally erase the contact’s original acquisition source.

A useful design is to keep original acquisition fields on the contact and submission-specific campaign details on each enquiry or activity record.

7. Test the complete journey

Use distinct test campaign values so records are easy to identify.

Test Expected result
Tagged landing page → contact form Campaign values reach the CRM
Tagged landing page → internal page → form Original campaign remains
Untagged visit with no stored campaign Campaign fields remain empty
Second tagged URL in the same page session First stored campaign remains
Storage unavailable Form still submits; persistence may be lost
Form hosted on another origin Explicit handoff is needed

Inspect the request, backend processing, and final CRM record.

A correct browser payload proves only the first stage worked.

Campaign capture is one piece of attribution

Preserving UTMs makes an enquiry easier to investigate. It does not guarantee that CRM reports will match analytics reports.

Analytics may use different attribution rules, session definitions, consent signals, and advertising integrations.

Keep your implementation’s meaning explicit:

These fields record the first tagged campaign captured in this tab’s page session.

That definition gives developers, marketers, and sales teams a shared understanding of what the data can explain.