For marketers · No developer needed

Know where every lead
really came from.

Intake is a small script you paste into Google Tag Manager once. After that, every form, every lead, and every report can answer the one question that matters: which campaign actually brought this customer?

Setup
5 minutes
One Custom HTML tag in GTM.
What you get
Source of every visit
UTM, referrer, organic, direct, paid
Remembers
The first touch
Even if the user comes back 10 times
Privacy
Consent-aware
Stores data only when the user agrees

What's inside this guide: how to install Intake in GTM and verify it works, your first win — sending the real source with every form, a quick-reference of everything Intake tracks, and advanced options for consent, PII hashing, and multi-touch attribution.

Stop losing the source.Normally UTM data disappears after one page. Intake keeps it in a cookie for months.
Fix "direct / none" in your CRM.Every lead arrives with its real source, campaign, and ad click ID attached.
See the full journey.First-touch, last-touch, and multi-touch — all ready to use in GA4.
Works with your tools.GTM, GA4, your CRM, Google Sheets, Telegram bots — anything that can read a variable.
Google Tag ManagerGA4Google AdsMeta AdsTikTokLinkedIn AdsMicrosoft AdsTwitter/X AdsPinterest AdsHubSpotPipedrive
Step 1

Install it with two GTM tags

Library in one tag, settings in another. Simple to set up, safe to update later.

The idea: the Library tag holds the code you never touch. The Config tag holds your settings. Link them with Tag Sequencing so the library always loads first.

Tag 1 — Intake Library (Custom HTML, no trigger)

Paste the full library code from intake.plurio.ai. Don't edit anything here — updates are painless this way.

// Intake library v2.x — paste the whole block from intake.plurio.ai
// DO NOT EDIT. Configuration lives in the "Intake — Config" tag.
!function(t,e){/* …minified library… */}(this, function(){ … });

Tag 2 — Intake Config (Custom HTML, trigger: All Pages)

The tag you actually edit. Start with this recommended config:

intk.init({
  // Hash emails/phones from forms (SHA-256) — off by default
  pii_collection: { enabled: true },

  // Capture GA Client ID for BigQuery stitching — off by default
  analytics_ids: { google_analytics: true },

  // Push intk_ready event + intk_user_profile to dataLayer
  data_layer: true,

  // Consent: respect your CMP (OneTrust, Cookiebot, Didomi…)
  consent_mode: {
    enabled: true,
    default_consent: 'denied',   // no cookies until user clicks "Accept"
    url_passthrough: true         // pass UTMs via URL while consent denied
  }
});
Why these? Everything else has good defaults (6-month cookies, 30-min sessions). But pii_collection, analytics_ids, and consent_mode are off by default — without them you lose email hashing, GA4 stitching, and GDPR compliance. See the Go Deeper section for domain, social referrals, and more.

Link them with Tag Sequencing

Open Config tag → Advanced Settings → Tag Sequencing. Check "Fire a tag before this tag fires" and pick Intake — Library. Turn on "Don't fire this tag if the setup tag fails". Save and publish.

Step 2

Verify it works

1

GTM Preview

Click Preview in GTM. Visit your site. In the debug panel, look for Intake — Config tag firing on "All Pages". Status: "Tag Fired".

2

Look for intk_ready in the dataLayer

In the GTM Preview panel, find the intk_ready event in the left sidebar. Click it — you should see intk_user_profile with traffic source data.

Remember this event. intk_ready fires every time Intake finishes loading. Any GTM tag that needs Intake data should use this as its trigger — not "Page View".
3

Console (optional)

Open DevTools (F12) and type intk.get.current.src. You should see your traffic source — (direct) if you typed the URL, google if you came from search.

Step 3

Send the real traffic source with every form

The highest-value use case: your CRM stops saying "direct / none" and starts showing the actual campaign behind every lead.

1Create a trigger for intk_ready

In GTM: Triggers → New → Custom Event. Name: Intake Ready. Event name: intk_ready. Save.

Why a separate trigger? Intake loads data asynchronously. The intk_ready event tells GTM "the data is ready now." If you read variables on Page View, they may still be empty.

2Create GTM Variables

Go to Variables → User-Defined Variables → New. Choose Data Layer Variable. Fill in the name from the table below.

Name it in GTMData Layer Variable NameGives you
Intake — Current Sourceintk_user_profile.traffic_attribution.current_visit.sourceVisit source
Intake — Current Mediumintk_user_profile.traffic_attribution.current_visit.mediumChannel
Intake — Current Campaignintk_user_profile.traffic_attribution.current_visit.campaignCampaign
Intake — First Sourceintk_user_profile.traffic_attribution.first_visit.sourceOriginal source
Intake — First Dateintk_user_profile.traffic_attribution.first_visit.timestampFirst visit date
Intake — Google Click IDintk_user_profile.identity.click_ids.googlegclid
Shorter alternative: you can also use JavaScript Variable type pointing to intk.get.current.src, intk.get.first.src, etc. Both work. Data Layer Variables are recommended because they pair naturally with intk_ready.

3Pass the data with every form submission

Option A — Hidden fields in the form (works with any form builder)

Create a Custom HTML tag in GTM with trigger Intake Ready. It automatically injects hidden fields into every form on the page:

// Fires on trigger: Intake Ready
document.querySelectorAll('form').forEach(function(form) {
  var fields = {
    'utm_source':   intk.get.current.src,
    'utm_campaign': intk.get.current.cmp,
    'first_source': intk.get.first.src,
    'gclid':        (intk.get.click_ids || {}).gclid || ''
  };
  Object.keys(fields).forEach(function(name) {
    var input = form.querySelector('input[name="' + name + '"]');
    if (!input) {
      input = document.createElement('input');
      input.type = 'hidden';
      input.name = name;
      form.appendChild(input);
    }
    input.value = fields[name];
  });
});

The script finds every <form> on the page, creates hidden fields if they don't exist yet, and fills them with Intake data. When the user submits, the values go to your CRM automatically.

Option B — GA4 event parameters (if you send form events via GTM)

In your GA4 Event tag, add the GTM variables from the table above as event parameters:

Parameter nameValue
utm_source{{Intake — Current Source}}
utm_campaign{{Intake — Current Campaign}}
first_source{{Intake — First Source}}
first_date{{Intake — First Date}}
gclid{{Intake — Google Click ID}}
Outcome: Every form submission now carries the real traffic source. Your sales team sees which campaign brought each lead. "Direct / none" becomes "google / cpc / spring_sale".
Recipe

Compare "first click" vs. "last click" in GA4

Most marketers only see the last click before conversion. With Intake you can send the first click too — and finally answer "which channel starts sales, and which one closes them?"

Add two custom dimensions to your GA4 conversion event tag:

first_touch = {{Intake — First Source}} / {{Intake — First Medium}}
last_touch  = {{Intake — Current Source}} / {{Intake — Current Medium}}

(Create Intake — First Medium: Data Layer Variable = intk_user_profile.traffic_attribution.first_visit.medium)

Outcome: A single GA4 report showing channels that introduce customers vs. channels that convert them.
Reference

Quick data reference

Every path below is a value you can read via GTM variables or in the console.

Current visit (the 5 UTM values)

JavaScript pathDataLayer path (intk_user_profile)=
intk.get.current.src.traffic_attribution.current_visit.sourceutm_source
intk.get.current.mdm.traffic_attribution.current_visit.mediumutm_medium
intk.get.current.cmp.traffic_attribution.current_visit.campaignutm_campaign
intk.get.current.cnt.traffic_attribution.current_visit.contentutm_content
intk.get.current.trm.traffic_attribution.current_visit.termutm_term

First visit

JavaScript pathDataLayer pathDescription
intk.get.first.src.traffic_attribution.first_visit.sourceThe very first source
intk.get.first.mdm.traffic_attribution.first_visit.mediumThe very first channel
intk.get.first.cmp.traffic_attribution.first_visit.campaignThe very first campaign
intk.get.first_add.fd.traffic_attribution.first_visit.timestampDate/time of the first visit
intk.get.first_add.ep.traffic_attribution.first_visit.landing_pageFirst landing page URL

Session & user

JavaScript pathDescription
intk.get.udata.vstHow many times this person has visited your site
intk.get.session.pgsPages seen in the current session

Ad click IDs — 11 platforms

JavaScript pathDataLayer (.identity.click_ids)Platform
intk.get.click_ids.gclid.googleGoogle Ads
intk.get.click_ids.fbclid.facebookMeta / Facebook
intk.get.click_ids.ttclid.tiktokTikTok Ads
intk.get.click_ids.msclkid.microsoftMicrosoft / Bing
intk.get.click_ids.li_fatid.linkedinLinkedIn Ads
intk.get.click_ids.twclid.twitterTwitter / X
intk.get.click_ids.snapclid.snapchatSnapchat Ads
intk.get.click_ids.pclid.pinterestPinterest Ads
intk.get.click_ids.dclidGoogle DV360
intk.get.click_ids.wbraidGoogle Ads (web-to-app)
intk.get.click_ids.gbraidGoogle Ads (app-to-web)
Advanced

Config options that solve real problems

The config above handles 80% of setups. Below are options you add when you hit a specific problem.

Problem

"I have a cookie consent banner (GDPR / ePrivacy)"

If you run a CMP (OneTrust, Cookiebot, Didomi…) and don't tell Intake, it will set cookies before consent — a compliance risk.

consent_mode: {
  enabled: true,              // wait for consent before storing cookies
  default_consent: 'denied',  // no cookies until "Accept"
  url_passthrough: true       // pass UTMs via URL while consent denied
}

Supports 10+ CMPs. With url_passthrough: true, UTMs and click IDs still travel between pages via the URL.

Problem

"My site is a Single Page App"

SPA tracking is on by default. If your site is NOT an SPA and you see duplicate views: spa_tracking: false. Hash-based routing: call intk.trackPageview() manually.

Problem

"I need to link anonymous visits to logged-in users"

user_id: { source: 'dataLayer', key: 'userId' }

Or set programmatically after login: intk.setUserId('user_123')


Full config template

Copy into your Config tag and uncomment what you need. Based on a real production setup.

intk.init({
  // ── [CUSTOMIZE] Domain ──────────────────────────
  domain: 'YOUR_DOMAIN.com',

  // ── Identity & Analytics ────────────────────────
  pii_collection: { enabled: true },
  analytics_ids: { google_analytics: true },

  // ── Social traffic classification ─────────────
  referrals: [
    { host: 'facebook.com',  medium: 'social', display: 'facebook'  },
    { host: 'instagram.com', medium: 'social', display: 'instagram' },
    { host: 'linkedin.com',  medium: 'social', display: 'linkedin'  },
    { host: 'x.com',         medium: 'social', display: 'twitter'   },
    { host: 'youtube.com',   medium: 'social', display: 'youtube'   },
    { host: 'tiktok.com',    medium: 'social', display: 'tiktok'    }
  ],

  // ── Consent — uncomment if you use a CMP ─────
  // consent_mode: { enabled: true, default_consent: 'denied', url_passthrough: true },

  // ── Uncomment what you need ────────────────────
  // user_id: { source: 'dataLayer', key: 'userId' },
  // spa_tracking: false,
  // timezone_offset: 1,
});

More features to explore

FeatureHow to use it
Multi-touch attributionintk.getAttribution('u-shaped') — 5 models: first, last, linear, U-shaped, time-decay
Touchpoint chainintk.get.touchpoints — the full journey across visits
PII capture eventsintk_email / intk_phone dataLayer events when forms capture contact info
Cross-domain linkslink_decoration config — decorate outbound links with UTM + click IDs
Consent withdrawalintk.withdrawConsent() — clears all Intake cookies instantly

Get started

Get the snippet & docs → intake.plurio.ai

Free & open-source → MIT License · no paid tiers, no tracking of you.

Supported CMPs: OneTrust, Cookiebot, Axeptio, Didomi, Sirdata, Termly, TrustArc, Iubenda, Klaro, Consentmo.