UTM Tracking -- Full Setup
UTM tracking is the cornerstone of attribution in DURUM.ai. Without it, your leads and sales appear in the dashboard but aren't linked to the right ads. This guide walks you through the setup step by step, regardless of the form tool you use.
Why It's Essential
When a prospect clicks on a Meta ad, the destination URL contains UTM parameters that identify the campaign and ad. These parameters are the only way for DURUM.ai to know which ad generated which lead.
The problem: these parameters live in the URL. If your form doesn't capture them, the information is lost when the prospect submits the form. The lead arrives in DURUM.ai, but it's impossible to link it to the right ad.
The solution relies on three complementary layers that reinforce each other.
The Three Tracking Layers
| Layer | Method | Reliability | Effort |
|---|---|---|---|
| Tracking script | DURUM.ai script on your landing pages | Very high | 1 line of code |
| Hidden form fields | Hidden fields in your CRM/form tool | Maximum | Manual configuration |
| Server enrichment | DURUM.ai looks up UTMs from previous sessions | High (fallback) | Automatic |
Recommendation
The combination of layers 1 and 2 gives near-perfect coverage. Layer 3 automatically catches cases where the first two failed.
Step 1 -- URL Parameters in Meta Ads
In your Meta ads, add the following parameters in the URL Parameters field (at the ad or account level). Copy this block as-is:
utm_source=Meta&utm_medium=paid&utm_campaign={{campaign.name}}&utm_content={{ad.name}}&utm_term={{adset.name}}&utm_campaign_id={{campaign.id}}&utm_ad_id={{ad.id}}&utm_adset_id={{adset.id}}The double curly braces (campaign.name, ad.name, etc.) are Meta macros that are automatically replaced with real values at click time. You don't need to do anything else on this side.
WARNING
Do not modify the parameter names (utm_source, utm_campaign, etc.). DURUM.ai expects these exact names. Changing utm_content to ad_name for example would prevent attribution.
Step 2 -- DURUM.ai Tracking Script
Add this line to the <head> of each of your landing pages. Replace YOUR_KEY with your DURUM.ai client_key.
<script src="https://app.durum.ai/api/track.js" data-client="YOUR_KEY"></script>What the Script Does
The script is lightweight (under 5 KB) and automatically performs the following operations:
- Captures UTM parameters from the URL on page load
- Stores values in a cookie (30 days) to cover multi-page visits
- Publishes the values into standard cookies that third-party tools read (see below)
- Injects values into your form's hidden fields
- Decorates outbound booking links and widgets with your UTMs
- Detects form submission and sends UTMs to DURUM.ai
The script works with all form tools: GHL, Typeform, Calendly, Jotform, Tally, WordPress, Webflow, ClickFunnels, and any standard HTML form.
Handing attribution to third-party tools
A cookie never crosses a domain boundary. The tools you embed therefore do not all read attribution the same way, and the script covers all three cases:
| Case | How the tool receives attribution | Examples |
|---|---|---|
| The tool runs JavaScript on your page | Standard cookies utm_source, utm_medium, utm_campaign, utm_term, utm_content and their first_utm_* counterparts | Embedded iClosed widget |
| The tool renders a form inside your page | Hidden fields filled automatically | GHL, WordPress, Webflow |
| The tool lives on its own domain | Parameters appended to the link or iframe URL | Calendly, Typeform, direct iClosed link |
Cookies are set on your registrable domain, so they stay readable from any of your subdomains. A funnel whose opt-in lives on go.example.com and whose booking page lives on www.example.com works with no extra configuration.
Script placement
Load the script in the <head>, before any third-party widget. It then writes the cookies before the widget tries to read them.
Attribution model: channel versus ad
The script separates two pieces of information that do not have the same lifetime, because they answer different questions.
The channel (utm_source, utm_medium) answers "where did the last click come from?". It follows the last touch. If your prospect comes back through an email, the channel becomes email.
The ad (utm_campaign, utm_term, utm_content and the ad platform IDs) answers "which ad produced this lead?". It is only ever replaced by another ad. An email click, a link in bio or an organic share never overwrite it.
In practice: a prospect clicks your Meta ad on Monday, does nothing, comes back Tuesday through your nurturing sequence and books. The attribution handed over is utm_source=email together with the original Meta campaign, ad set and ad. You know both which ad paid for the lead and which channel triggered the booking.
A consequence worth knowing
The channel and the ad can come from two different clicks. In the dashboard, splitting revenue by source will credit the email, while splitting it by campaign will credit the ad. Those two views are therefore not two slices of the same total.
The first_utm_* fields keep the contact's very first known touch, for one year.
Tag your emails as owned channels
If you would rather keep the ad credited as the source, tag your email and SMS links with utm_channel=email instead of utm_source=email. The script then treats that click as an assist and does not touch ad attribution at all.
Smart Detection
The script detects form submissions via six different methods, allowing it to work even with the most exotic forms:
- Native HTML submission (standard forms, GHL, WordPress)
- Typeform iframe (via postMessage)
- Calendly iframe (via postMessage)
- AJAX and fetch calls (SPA forms, React, Vue)
- Thank-you pages (detection via MutationObserver)
- URL changes (SPA redirects to /thanks, /thank-you)
First-party domain (CNAME)
By default the script is served from app.durum.ai, a third-party domain. Ad-blockers and Safari/iOS tracking protection can then block part of the measurement, especially page views.
For maximum coverage (from about 70-85% to 90-95%), serve the pixel first-party from a subdomain of your own site. Two steps:
1. Add a DNS record at your registrar (where your domain is managed):
| Type | Name | Value |
|---|---|---|
| CNAME | trk | cname.vercel-dns.com |
This creates trk.your-domain.com. DURUM adds the domain and issues the SSL certificate automatically.
2. Replace the script with its first-party version (no data-client needed, the domain resolves your account):
<script src="https://trk.your-domain.com/api/track.js"></script>The script and beacons now live on a subdomain of your own site: they resist blockers that filter by third-party domain, and Safari/Firefox treat them as first-party.
What stays out of reach
Even first-party, no JavaScript pixel counts 100% of page views (JS disabled, signature-based blockers, consent declined). For an exhaustive total identical to your funnel platform's (which counts server-side), reconcile against the platform's server-side total. The pixel gives you the source of each page view.
Consent
The first-party pixel respects consent exactly like the standard version: no page view is collected if the visitor declined (Law 25 / GDPR). Getting past ad-blockers never authorizes bypassing consent.
Step 3 -- Hidden Fields in Your Forms
The tracking script covers most cases, but adding hidden fields to your forms provides an extra safety layer. It's also the only way to guarantee your CRM stores UTMs directly in the contact.
Naming Convention
Field names must be exactly as in this table. Respect lowercase and underscores.
| Field Name | Description | Required |
|---|---|---|
utm_source | Traffic source (Meta, Google, etc.) | Yes |
utm_medium | Traffic type (paid, organic, email) | Yes |
utm_campaign | Campaign name | Yes |
utm_content | Ad name | Yes |
utm_term | Ad set name | Yes |
utm_campaign_id | Meta campaign numeric ID | Recommended |
utm_ad_id | Meta ad numeric ID | Recommended |
utm_adset_id | Ad set numeric ID | Recommended |
WARNING
Field names are case-sensitive. UTM_Source or utmSource won't be recognized. Always use the lowercase version with underscores.
Configuration by Platform
GoHighLevel (GHL)
- Go to Settings > Custom Fields > Contact
- Create each field from the table above as Single Line Text
- Name each field exactly as in the table (e.g.,
utm_source) - In your form or capture page, add these fields in Hidden mode
- GHL supports native pre-fill via URL parameters: fields named
utm_source,utm_campaign, etc. are automatically filled from the URL
TIP
With GHL, the DURUM.ai script and custom fields are complementary. The script fills hidden fields on page load, and GHL saves them in the contact at submission time.
Typeform
- In your form settings, go to Hidden Fields
- Add the following fields:
utm_source,utm_medium,utm_campaign,utm_content,utm_term,utm_campaign_id,utm_ad_id,utm_adset_id,client_key - Typeform automatically extracts values from URL parameters
- Set
client_keyto your DURUM.ai client key
Calendly
Calendly automatically captures standard UTM parameters (utm_source, utm_medium, utm_campaign, utm_content, utm_term) from the URL of the page where the embed is placed. No additional configuration needed.
The DURUM.ai script completes the tracking by capturing Meta identifiers (utm_ad_id, utm_campaign_id) that Calendly doesn't natively support.
Jotform
- Add Hidden Field elements to your form
- Set the Field Name to the exact name (e.g.,
utm_source) - Check Prepopulate and set the Query Parameter Name to the same name
Tally
- In the form settings, add Hidden Fields
- Name each field exactly as in the table
- Tally automatically captures corresponding URL parameters
WordPress / Elementor
- Add Hidden type fields to your form
- Set the Default Value to URL Parameter and specify the name (e.g.,
utm_source)
Webflow
- Add
<input type="hidden">fields to your form - Set the
nameattribute to the exact UTM name - The DURUM.ai script will automatically fill these fields
Custom HTML
If you use a custom HTML form, add these hidden fields between the <form> tags:
<input type="hidden" name="utm_source">
<input type="hidden" name="utm_medium">
<input type="hidden" name="utm_campaign">
<input type="hidden" name="utm_content">
<input type="hidden" name="utm_term">
<input type="hidden" name="utm_campaign_id">
<input type="hidden" name="utm_ad_id">
<input type="hidden" name="utm_adset_id">
<input type="hidden" name="client_key" value="YOUR_KEY">The DURUM.ai script will automatically inject values from the UTM cookie on page load.
Step 4 -- Test Your Configuration
DURUM.ai provides a test endpoint to verify your UTMs are being transmitted correctly.
Test via URL
Open this URL in your browser, replacing the values:
https://app.durum.ai/api/webhook/test-utm?client_key=YOUR_KEY&utm_source=Meta&utm_medium=paid&utm_campaign=test_campaign&utm_content=test_ad&utm_term=test_adsetTest via Webhook
Send a test webhook identical to what your form would send:
POST https://app.durum.ai/api/webhook/test-utm?client_key=YOUR_KEY
Content-Type: application/json
{
"email": "test@example.com",
"utm_source": "Meta",
"utm_medium": "paid",
"utm_campaign": "test_campaign",
"utm_content": "test_ad",
"utm_term": "test_adset"
}Understanding the Response
The response contains a grade from A+ to F and a breakdown of what was found:
| Grade | Meaning |
|---|---|
| A+ | All required and recommended fields are present |
| A | All required fields are present |
| B | Most required fields are present |
| C | Some required fields are missing |
| D/F | Incomplete configuration, attribution will be limited |
The response also indicates missing fields and any warnings to correct your configuration.
Troubleshooting
My leads arrive but aren't attributed
- Verify UTM parameters are present in the destination URL of your Meta ads
- Verify the DURUM.ai script is properly installed on the landing page
- Test with the
/api/webhook/test-utmendpoint to see which fields are received - Verify the hidden field naming convention in your form
UTMs contain unresolved placeholders
If you see values like campaign_name or unresolved curly braces instead of real values, it means Meta macros weren't resolved. Verify UTM parameters are configured at the right level in Meta Ads Manager (at the ad level, not the account level).
The tracking script doesn't detect the submission
Some highly customized forms may escape automatic detection. In that case, hidden form fields take over. Make sure you've configured hidden fields as explained in your platform's section.
I use a tool that isn't listed
The DURUM.ai script works with any standard HTML form. If your tool generates a standard <form> or uses fetch/XMLHttpRequest to submit data, tracking will work. When in doubt, add HTML hidden fields and the script will handle the rest.
Summary
For complete attribution in DURUM.ai:
- Add UTM parameters to your Meta ads
- Install the tracking script on your landing pages (1 line)
- Add hidden fields to your forms (exact naming required)
- Test with the validation endpoint
These three steps ensure every lead, booking, and sale is correctly attributed to the ad that generated it.