The Short Answer
Use GHL's External Tracking script to see what people look at on your site, and send every form to GHL from your server as well. The script is easy, it's one line of code. But it runs in the visitor's browser, so an ad blocker can stop it, and it only picks up the form fields it recognises. Sending the lead from your server has neither of those problems. I run both, and in my first test that's exactly what saved a lead.
Option 1: GHL's External Tracking Script
You'll find it in your sub-account under Settings, then External Tracking. Copy the script and paste it just before the closing </body> tag on every page. The tracking ID inside it is public (anyone can see it in your page source), so it's fine to put it straight into your code.
After that it does three things:
- Records page views, anonymously at first
- Captures submissions from your own forms and creates or updates the contact in GHL
- Once someone submits a form, links their earlier page views to their contact, along with where they came from
One setting tripped me up. The Settings tab has three toggles, and "Form Analytics" sounds like the one you want. It isn't. Form Analytics only tracks how people interact with a form, it doesn't create any contacts. You need "Form Submissions" turned on for that. Captured submissions show up under Sites, Forms, Submissions, when you filter by "External Forms".
The script is also picky about which forms it can read:
- It has to be a real
<form>element. Forms inside an iframe or a popup widget won't be picked up - Every field you want captured needs a
nameattribute, and it can't be disabled - You need
<input type="email" name="email">, because email is how GHL matches the submission to a contact - Use a normal submit button. Running your own JavaScript on submit is fine, as long as it doesn't block the form's submit event
What went wrong on my site
After installing it, I sent two test leads and checked GHL. The one from my homepage form came through with the name and email. The one from a case study page came through with the email only. No name.
Both forms called the field name. The only real difference was the label. On the homepage it said "Full Name", and on the case study pages it just said "Name". GHL matches form fields by the name attribute, the label, or an existing custom field, and name on its own isn't a GHL field. So the homepage form matched through its label, and "Name" didn't match anything. GHL just dropped it and carried on.
I changed the label to "Full Name" on all eight case study pages. The bigger point is that the script won't tell you what it skipped. Look at a real submission inside GHL before you trust it.
If you need to debug it, add data-debug="true" to the script tag and open your browser console. Every step gets logged with [LC Tracking] in front of it: which forms it found and which events it sent. Take it off again before you go live.
Option 2: Send the Lead From Your Server
This is the one that doesn't depend on the visitor's browser at all. When someone submits a form, the site also sends the lead to a small backend function, and that function calls the GHL API using a Private Integration Token. I use a Cloudflare Pages Function for this, but a WordPress hook, a Next.js API route or an n8n webhook will do the same job. Just keep the token on the server. If it sits in your front-end code, anyone can read it.
My function makes three calls:
POST /contacts/upsertcreates the contact, or updates it if that email already existsPOST /contacts/{id}/tagsadds the tags. Don't put tags inside the upsert itself. That replaces the contact's whole tag list, so a repeat enquiry wipes out their historyPOST /contacts/{id}/notessaves the message, the page it came from and the traffic source as a note
You need a developer to set this up, and it won't give you page views. But no ad blocker can touch it, and you decide exactly where each field goes.
What went wrong here too
My forms also send me an email through a form service. In my original code, the call to GHL only ran after the email service replied. So if the email service ever failed, the code went straight to the error message and never called GHL. The lead wasn't in the CRM, and nothing told me.
The fix was to send the lead to GHL first, on its own, before waiting for anything else. I also set the browser's keepalive option, so the request still goes through even if the visitor closes the tab. If your CRM sync only runs after some other call succeeds, check it. It's a common way to set things up, and it fails silently.
Option 3: Embed a GHL Form
This is the no-code option. Build the form inside GHL, copy the embed code and paste it on your page. Submissions go straight into GHL with tags and workflow triggers, and you don't write any code.
The downsides: it loads in an iframe, so it only partly matches your site's design, it slows the page down a little, and the External Tracking script can't see inside it. I'd use it for a WordPress or Wix site that has no developer. I wouldn't use it on a custom-designed page where the form needs to look like it belongs there.
Option 4: A Webhook Into a GHL Workflow
If your forms come from another tool like Typeform, Jotform or Webflow's own forms, send them to GHL with a webhook. You can point it straight at a GHL workflow that starts with an Inbound Webhook trigger, or run it through n8n, Make or Zapier first if the data needs cleaning up.
The Inbound Webhook trigger is one of GHL's premium features. It costs about $0.01 each time it runs, or around $10 a month for 10,000 runs on the Workflow Pro add-on. For normal lead volumes that's next to nothing. The real cost is having one more thing that can break.
Side by Side
| Method | Page views | Works with ad blockers | Needs a developer | Best for |
|---|---|---|---|---|
| External Tracking script | Yes | No | No | Seeing what a lead read before contacting you |
| Server-side API call | No | Yes | Yes | Making sure every lead lands |
| Embedded GHL form | Only on the form | Mostly | No | Sites without a developer |
| Webhook into a workflow | No | Yes | Some setup | Forms from other tools |
What I Run on zam88.io
Options 1 and 2, together. Every form on this site sends the lead to my API function first, and then GHL's script picks up the same submission in the browser. Both update the same contact, matched by email.
My first two test leads showed why I run both. The homepage lead came through both routes complete. On the case study lead, the script missed the name, but the contact in GHL still had it, because the API call had already saved it. One route covered for the other before I had even found the problem.
Before You Go Live
- The email field is
type="email"andname="email"on every form - Labels are ones GHL understands, like "Full Name" instead of just "Name"
- "Form Submissions" is switched on in External Tracking, not only "Form Analytics"
- Duplicate contacts are switched off in your sub-account, so both routes end up on one contact
- Your privacy policy mentions the trackers you use. GHL's script uses cookies and browser storage like any analytics tool
- You've sent a real test lead and checked it in GHL, not just in the browser console
It also helps to give each route its own tag, so later you can see which one is doing the work. If your tags are already a mess, I wrote a guide to tags, custom fields and custom values that goes through how to set them up.
Which One Should You Use?
A simple site with no developer:
- Embed a GHL form, and add the tracking script for page views
A custom site where every lead matters:
- Send leads from your server, and use the tracking script for page views
Forms coming from another tool:
- A webhook into a GHL workflow, directly or through n8n
Whichever you pick, test it with a real lead and check the contact in GHL. On day one, both of my setups looked fine in the browser, and only one of them had actually saved everything.
If you're not sure all your website leads are reaching GoHighLevel, book a free strategy call with me. I'll find where they're getting lost and set it up properly.