Lead source capture for GoHighLevel

A small working demo by No BS AI. It shows how a lead form can keep the ad source (UTMs, Facebook click ID, referrer) so the data still lives on the contact and the opportunity in GoHighLevel after forms, calendars and workflows.

Try it: open this page with test UTMs, for example Ads Manager link Boosted post link No UTMs Nothing is sent anywhere. Submitting only shows the payload that would go to the webhook.

1. Lead form

2. What the hidden fields hold

First touch is saved once and never overwritten. Last touch updates on every visit that carries UTMs or a click ID. Both live in localStorage, so a lead who clicks an ad today and books next week still carries the source.

3. Payload sent to the n8n webhook

Submit the form to see the payload.

4. How it maps into GoHighLevel

Form / script valueGHL contact custom field (key)Where it is used
ft_source / ft_medium / ft_campaigncontact.first_touch_source / first_touch_medium / first_touch_campaignNever overwritten. Answers "which ad first brought this person in".
lt_source / lt_medium / lt_campaigncontact.last_touch_source / last_touch_medium / last_touch_campaignUpdated on each form fill. Answers "what made them convert".
utm_contentcontact.utm_contentAd or post creative level reporting.
fbclid / fbccontact.fbclid / contact.fbcSent back with Conversions API events to match Meta clicks.
referrer / landing_pagecontact.referrer / contact.landing_pageDebugging and organic traffic.
lt_source + lt_campaignContact "source" (standard field)Shows in the contact record and in GHL source reports.
first touch fieldsOpportunity source + opportunity custom fieldsA workflow copies the contact fields onto the opportunity when it is created (form, calendar booking or manual), so revenue reports by source work even after the lead moves stages.

Key rule: calendars and later forms often create or update the contact without UTMs. Writing to dedicated first touch fields that workflows never overwrite is what stops the source from dropping.

5. UTM naming convention

ParameterRuleAds Manager exampleBoosted post example
utm_sourcePlatform, lowercasefacebook or instagramfacebook or instagram
utm_mediumType of trafficpaid_socialboosted_post
utm_campaignyyyy-mm_offer_goal2026-11_coach-webinar_lead2026-11_boost_testimonial-post
utm_contentCreative or post IDvideo-a_hook1 (or {{ad.name}})post-1103
utm_termAudiencelookalike-1pct (or {{adset.name}})followers

Ads Manager supports dynamic URL parameters such as {{campaign.name}} and {{ad.name}} in the URL parameters box. Boosted posts usually do not give you the same control, so links in the post or button need the UTMs typed in by hand. When a boosted post sends people to Messenger, Instagram DM or an instant form, there is no page visit, so UTMs cannot be captured on a website and the source has to be set in the GHL workflow or integration instead.

6. The n8n side

The matching n8n workflow (in the public repo ghl-attribution-demo by 369Apps on GitHub) does this:

  1. Webhook receives the form payload above.
  2. Code node cleans the values and builds the GHL custom field list.
  3. HTTP node calls the GHL API contact upsert (by email or phone) with source and custom fields.
  4. First touch fields are only written when empty, so a returning lead keeps the original source.