Capture gclid (and every other click ID) in your forms
The click ID lives on the landing page URL. Your form lives three clicks later. Bridging that gap is the whole game.
What a click ID is, and why your spreadsheet wants one
When someone clicks an ad, the ad platform appends an opaque identifier to the destination URL. That identifier is the thread connecting the eventual lead back to the exact click - campaign, ad group, keyword, cost. If it reaches your spreadsheet next to the lead's email, you can judge campaigns by revenue instead of by form fills. If it does not, attribution stops at "they filled out the form."
The IDs you will meet:
gclid- Google Ads. Appended automatically when auto-tagging is on. Also the key for uploading offline conversions back into Google Ads.wbraidandgbraid- Google's privacy-preserving variants, introduced for iOS traffic after App Tracking Transparency. When a fullgclidcannot be passed,wbraidshows up for web conversions andgbraidfor web-to-app. You must capture all three; iOS traffic often carries only these.fbclid- Meta. Appended to outbound clicks from Facebook and Instagram.msclkid- Microsoft Advertising, with auto-tagging enabled.
All of them are query parameters. That is the strength (nothing to install on the ad side) and the weakness, as the next section shows.
The multi-page problem: why the ID never reaches the form
The parameter exists in exactly one place: the URL of the first page the visitor lands on. The moment they click through to /pricing and then /contact, the query string is gone. Browsers do not carry query parameters across navigations, and your contact form on a clean URL has nothing to read.
This is why so many "capture gclid in a hidden field" tutorials quietly fail in production. The hidden-field-reads-the-URL trick works only when the ad lands directly on the page with the form. Real journeys rarely cooperate: ads land on campaign pages, visitors browse, and the form is somewhere else entirely. The result is a Leads sheet where the gclid column is filled only for the minority who converted on the landing page - biased data that undercounts every campaign that drives considered visits.
The manual fix, and where it leaks
The classic solution is a snippet on every page: on load, read the known parameters from location.search, stash them in storage, and on the form page, copy them into hidden inputs before submit. It can work. It also has recurring leaks:
- Someone adds a new page template without the snippet, and that path silently loses attribution.
- The hidden inputs must exist in every form, spelled exactly right, forever.
- Single-page apps render forms after the snippet ran, so the copy-into-inputs step never fires.
- Nobody notices any of this for months, because missing attribution looks like organic traffic, not like an error.
It is maintenance-heavy glue code for something that should be infrastructure.
How sl.js closes the gap
SheetLink Forms ships this as part of the embed. One script tag, sitewide:
<script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken"></script>On every page load, sl.js checks the URL for gclid, wbraid, gbraid, fbclid, msclkid, and the five utm_* parameters (source, medium, campaign, term, content). Anything present is persisted in localStorage. When any bound form submits - pages later, on a different visit path - the stored values are attached to the submission automatically. No hidden inputs to author, no per-page snippet, no template that can forget.
Two details do real work here. First, persistence is in localStorage, so the values survive navigation across your whole site rather than living and dying with one page. Second, a MutationObserver watches for forms rendered after load, so single-page apps and late-injected embeds are bound the same as static markup. The submission also carries the page URL, the referrer, and a timing signal used for spam screening - the mechanics are on how it works and in the docs.
Working setup
A form on any page of the site, with the embed loaded once:
<script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken"></script>
<form data-sheetlink="slf_yourtoken">
<input name="name" placeholder="Name" required>
<input name="email" type="email" placeholder="Work email" required>
<input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
<button type="submit">Request a demo</button>
</form>Forms whose action already points at your /f/ endpoint are bound automatically; data-sheetlink marks any other form. The honeypot is injected by sl.js when missing, but including _slhp explicitly keeps the form spam-safe even if the script is blocked. Submits happen over AJAX with inline success text (customizable with data-success-message, or navigate with data-redirect).
Test it: open any page as https://yoursite.com/some-page?gclid=TEST123, browse to a different page, submit the form there, and watch the row land with TEST123 in the gclid column. The live demo shows rows landing in a public sheet if you want to see the end state first.
What to do with the columns
Each captured parameter becomes its own column alongside your form fields. From there:
- Campaign truth: pivot leads (and, once you mark them, closed deals) by
utm_campaign- judged on outcomes, not clicks. - Offline conversion import: Google Ads accepts uploads keyed by
gclid,wbraid, orgbraid. Your sheet already has the ID and the timestamp per lead, which is most of the upload file. - Channel splits:
fbclidandmsclkidpresence tells you which paid channel produced a row even when UTMs were misconfigured on the ad.
Edge cases and honest limits
- Organic visitors have no click ID. Empty click-ID columns are correct data, not a bug - they are your non-paid segment.
- Capture the braid pair or undercount iOS. Campaigns heavy on Instagram and iOS Safari will often show
wbraid/gbraidinstead ofgclid. If a tool only capturesgclid, iOS conversions vanish from attribution. - localStorage is per-browser. A visitor who clicks the ad on their phone and converts on their laptop cannot be stitched by any client-side method.
- Consent: click IDs are pseudonymous identifiers tied to ad activity. If your consent banner gates marketing storage, load
sl.jsunder the same policy as your other marketing tags. - Auto-tagging must be on in Google Ads and Microsoft Advertising, or the platforms never append the parameter in the first place.
SheetLink Forms is free during beta, invite required - the waitlist is on the homepage.
FAQ
Why is my gclid column empty for some leads?
Three usual causes: the visitor was organic (no ad click, so no ID - correct behavior), auto-tagging is off in the ad platform, or the click happened on a different device or browser than the conversion. Check that iOS leads are landing in the wbraid/gbraid columns rather than gclid.
What is the difference between wbraid and gbraid?
Both are Google's privacy-preserving substitutes for gclid on iOS traffic affected by App Tracking Transparency. wbraid is used for web-to-web conversion measurement; gbraid covers web-to-app. sl.js captures both automatically, alongside gclid.
Do I need hidden fields in my form for the click IDs?
No. sl.js persists the parameters in localStorage at first touch and attaches them at submit time. You add one script tag sitewide; the form markup stays clean. Hidden-field setups keep working if you already have them, but they are no longer the mechanism.
Does this work in a React, Vue, or other single-page app?
Yes. A MutationObserver watches for forms rendered after initial load and binds them the same way, so client-side routing and late-mounted components are covered without extra wiring. See the docs for the event hooks (sheetlink:success, sheetlink:error) if you want to react in app code.
How long do captured parameters persist?
They live in the browser's localStorage, so they survive page navigations and later sessions in the same browser until storage is cleared. A returning visitor who first arrived through an ad still carries the click ID when they finally convert.
Can I use the captured gclid for Google Ads offline conversion import?
Yes - that is one of the main reasons to capture it. Export the gclid (or wbraid/gbraid) column with your conversion timestamps and values, and upload to Google Ads offline conversions. Your sheet already holds the per-lead key the upload requires.
Related guides: Send an HTML form to Google Sheets without a backend ยท Forms to a spreadsheet without Zapier (and why you might want that)