FreeGoogle Tag Manager recipes

Track ClickFunnels Form Submissions in Google Tag Manager with this Free GTM Recipe

A free, tested Google Tag Manager recipe that detects successful form submissions on your ClickFunnels funnels and gives you a trigger for sending conversions to Google Ads, Google Analytics and more.

Aaron Beashel
Aaron BeashelUpdated October 20267 min read
The ClickFunnels recipe tag inside Google Tag Manager

Want to send a conversion to Google Ads, Google Analytics, etc. whenever someone completes a form on one of your ClickFunnels funnel steps?

The problem you might have run into is that ClickFunnels opt-ins are awkward to track. When a visitor clicks the opt-in button, ClickFunnels doesn't submit the form the normal way. It sends the contact to its own servers in the background, waits to hear whether the details were accepted, and only then moves the visitor on to the next step in the funnel. This means that Google Tag Manager's built-in Form Submission trigger doesn't work, and to get it properly set up you need to write custom code that waits for ClickFunnels to accept the contact and sends an event to the dataLayer (and then you need to build a Trigger in GTM on top of that event).

Unless you write code for a living or know Google Tag Manager inside out, that's going to be a struggle.

Thankfully, there's an easier option.

Import this ClickFunnels Conversion Tracking Recipe into your Google Tag Manager account and you'll have everything you need to get basic conversion tracking working.

What Is a Google Tag Manager Recipe and What's in It?

Think of a recipe as a starter kit for Google Tag Manager. It's a tag, a trigger and a variable that you can import into your account rather than building each part by hand.

This recipe detects successful ClickFunnels opt-ins, passes a 'Form Submitted' event to Google Tag Manager, and gives you a Trigger you can use in GTM to send conversions to Google Analytics, Google Ads and more. It's made up of three parts and everything is outlined below so you can review each piece before importing it into your container.

Custom HTML Tag

Waits for ClickFunnels to confirm it has accepted the opt-in, then pushes a form submission event to the dataLayer. It fires on All Pages, once per event, and this is the code inside it:

The Tag's CodeJavaScript · 192 lines
/* ClickFunnels Form Listener by Converly. Pushes only event and form metadata. */
(function() {
  'use strict';

  window.dataLayer = window.dataLayer || [];
  var sink = {
    siteKey: 'gtm-pack-clickfunnels-event-only',
    advertisingAllowed: function () { return true; },
    onSubmission: function (payload) {
      var context = payload.submissionContext || {};
      var push = { event: 'clickfunnels_form_submitted', page_path: payload.pageUrl };
      window.dataLayer.push(push);
    }
  };

  var ATTEMPT = 'cf:form_submitted';
  var SUCCESS = 'cf:form_submitted:ok';
  var WINDOW_MS = 60000;
  var KEYS = ['email', 'first_name', 'last_name', 'name', 'phone_number'];
  var TARGETS = { email: 'email', first_name: 'firstName', last_name: 'lastName', phone_number: 'phone' };
  var active = null;

  var attempts = typeof WeakMap === 'function' ? new WeakMap() : null;

  function consentAllows() {
    try {
      var c = sink;
      return !(c && typeof c.advertisingAllowed === 'function' && c.advertisingAllowed() === false);
    } catch (e) { return true; }
  }

  var SENSITIVE_AUTOCOMPLETE = {
    'current-password': true, 'new-password': true, 'one-time-code': true,
    'cc-number': true, 'cc-csc': true, 'cc-exp': true, 'cc-exp-month': true,
    'cc-exp-year': true, 'cc-name': true, 'cc-given-name': true,
    'cc-family-name': true, 'cc-additional-name': true, 'cc-type': true,
  };

  var SENSITIVE_NAME_SUBSTRINGS = [
    'password', 'passwd', 'passcode', 'cardnum', 'ccnumber', 'creditcard',
    'onetimecode', 'securitycode', 'socialsecurity',
  ];
  var SENSITIVE_NAME_TOKENS = {
    otp: true, cvv: true, cvc: true, ssn: true, pwd: true, pin: true,
  };

  function isSensitiveName(name) {
    if (!name) return false;
    var lower = String(name).toLowerCase();
    var norm = lower.replace(/[^a-z0-9]/g, '');
    for (var i = 0; i < SENSITIVE_NAME_SUBSTRINGS.length; i++) {
      if (norm.indexOf(SENSITIVE_NAME_SUBSTRINGS[i]) !== -1) return true;
    }
    var parts = lower.split(/[^a-z0-9]+/);
    for (var p = 0; p < parts.length; p++) {
      if (SENSITIVE_NAME_TOKENS[parts[p]] === true) return true;
    }
    return false;
  }

  function isSensitiveField(type, autocomplete, name) {
    if (type === 'password') return true;

    if (autocomplete) {
      var tokens = autocomplete.split(/\s+/);
      for (var t = 0; t < tokens.length; t++) {
        if (SENSITIVE_AUTOCOMPLETE[tokens[t]] === true) return true;
      }
    }
    if (isSensitiveName(name)) return true;
    return false;
  }

  function labelTextFor(el) {
    var text = '';
    var wrap = el.closest('label');
    if (wrap) text += ' ' + wrap.textContent;
    var doc = el.ownerDocument;
    var id = el.getAttribute('id');
    var labels = doc.getElementsByTagName('label');
    for (var i = 0; id && i < labels.length; i++) {
      if (labels[i].getAttribute('for') === id) text += ' ' + labels[i].textContent;
    }
    var ids = (el.getAttribute('aria-labelledby') || '').split(/\s+/);
    var nodes = doc.querySelectorAll('[id]');
    for (var k = 0; k < nodes.length; k++) {
      if (ids.indexOf(nodes[k].getAttribute('id')) !== -1) text += ' ' + nodes[k].textContent;
    }
    return text;
  }

  function object(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
  }

  function dataProperty(objectValue, key) {
    var descriptor = Object.getOwnPropertyDescriptor(objectValue, key);
    return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor : null;
  }

  function contactFor(detail) {
    if (!object(detail) || 'purchase' in detail) return null;
    var contact = dataProperty(detail, 'contact');
    return contact && object(contact.value) ? contact.value : null;
  }

  function carrierForRuntime() {
    var events = window.CFEvents;
    if (!events || events.FORM_SUBMITTED !== ATTEMPT || events.FORM_SUBMITTED_OK !== SUCCESS) return null;
    var carriers = document.querySelectorAll('form#cfAR[data-cf-form-action]');
    return carriers.length === 1 && document.documentElement.contains(carriers[0]) ? carriers[0] : null;
  }

  function clearAttempt() {
    if (active) window.clearTimeout(active.timer);
    active = null;
  }

  function refreshIndex(index) {
    var fields = document.querySelectorAll('.elFormItem:not([data-prevent-submit="true"])');
    for (var i = 0; i < fields.length; i++) {
      var el = fields[i];
      if (el.closest('form#cfAR, [data-page-element="Checkout/V2"]')) continue;
      var name = el.getAttribute('name') || '';
      if (KEYS.indexOf(name) === -1) continue;
      var type = (el.getAttribute('type') || 'text').toLowerCase();
      var autocomplete = (el.getAttribute('autocomplete') || '').toLowerCase();
      var sensitive = isSensitiveField(type, autocomplete, name) ||
        isSensitiveName(el.getAttribute('id')) ||
        isSensitiveName(el.getAttribute('aria-label')) ||
        isSensitiveName(el.getAttribute('placeholder')) || isSensitiveName(labelTextFor(el));
      var ineligible = el.tagName === 'SELECT' || !/^(text|email|tel|name|first_name|last_name|phone_number)$/.test(type);
      var entry = index[name] || { sensitive: false, ineligible: false, type: type };
      entry.sensitive = entry.sensitive || sensitive;
      entry.ineligible = entry.ineligible || ineligible;
      index[name] = entry;
    }
  }

  function handleAttempt(event) {
    try {
      if (!consentAllows()) { clearAttempt(); return; }
      if (event.target !== document || !attempts) return;
      var carrier = carrierForRuntime();
      if (!carrier) { clearAttempt(); return; }
      var detail = event.detail;
      if (!contactFor(detail)) { clearAttempt(); return; }
      if (attempts.has(detail)) return; // no deadline extension or replay
      clearAttempt();
      var token = {};
      attempts.set(detail, token);
      active = { token: token, carrier: carrier, path: window.location.pathname,
        started: Date.now(), index: Object.create(null), timer: null };
      refreshIndex(active.index);

      active.timer = window.setTimeout(clearAttempt, WINDOW_MS + 1);
    } catch (e) { clearAttempt(); }
  }

  function snapshot() { return { pageUrl: window.location.pathname, userData: {}, formFieldsSeen: [] }; }

  function handleSuccess(event) {
    try {
      if (!consentAllows()) { clearAttempt(); return; }
      if (event.target !== document || !active) return;
      if (carrierForRuntime() !== active.carrier || window.location.pathname !== active.path ||
          Date.now() - active.started > WINDOW_MS) { clearAttempt(); return; }
      var detail = event.detail;
      if (!object(detail) || attempts.get(detail) !== active.token) return;
      var contact = contactFor(detail);
      if (!contact || !sink || typeof sink.onSubmission !== 'function') {
        clearAttempt(); return;
      }
      refreshIndex(active.index);
      var payload = snapshot(contact, active.index);
      clearAttempt(); // consume before invoking outside code, including reentry
      sink.onSubmission(payload);
    } catch (e) {
      clearAttempt();

    }
  }

  if (typeof window !== 'undefined' && !window.__converlyRecipeClickFunnels && attempts) {
    window.__converlyRecipeClickFunnels = true;
    document.addEventListener(ATTEMPT, handleAttempt);
    document.addEventListener(SUCCESS, handleSuccess);
    window.addEventListener('checkout:order-submit-errors', clearAttempt);
    window.addEventListener('payments:submit-failed', clearAttempt);
    window.addEventListener('pagehide', clearAttempt);
  }
})();

Variable

Stores the step of the funnel the lead opted in from, so you can tell your steps apart in any tag or trigger condition. These are its exact settings:

Name

DLV - ClickFunnels Page Path

Variable Type

Data Layer Variable

Data Layer Variable Name

page_path

Trigger

Fires whenever the form submission event comes through. You can use it as the trigger for any of your conversion tags. These are its exact settings:

Name

CE - ClickFunnels Form Submitted

Trigger Type

Custom Event

Event Name

clickfunnels_form_submitted

Download This Recipe

Import this file into Google Tag Manager and it gives you a ready-to-use listener tag, variable and trigger

Built from Converly's own ClickFunnels detection code and regularly tested on a live ClickFunnels 2.0 funnel

Why It's the Best Recipe Out There

It comes from the same detection code that powers our own conversion tracking product, Converly, reworked so it runs inside Google Tag Manager.

Only counts an opt-in after ClickFunnels confirms it accepted the contact. So an opt-in that ClickFunnels rejects (because they missed a required field, for instance) is never counted.
Has been tested using the same infrastructure and process as our own conversion tracking product, which has detected thousands of form submissions for Converly customers.

How to Use the ClickFunnels GTM Recipe

1

Install the Recipe

First, download the recipe using the form above. Then open Google Tag Manager, go to Admin and pick Import Container. Select the file, choose Merge so nothing already in your container gets overwritten, and confirm. The listener, the variable and the trigger land in your workspace automatically.

Importing the recipe container in Google Tag Manager
2

Configure Your Tags

Once the recipe is installed, you can use its Trigger to fire your conversion tags. Below are the setup instructions for the two most common destinations:

Example 1: Google Analytics

Create a new GA4 event tag, name the event something like generate_lead, and under Triggering pick the recipe's trigger (called CE - ClickFunnels Form Submitted). This will send an event to GA4 every time someone completes a ClickFunnels Opt-In form.

A GA4 event tag using the recipe's trigger

Example 2: Google Ads

Add a Google Ads conversion tag, input your conversion ID and label, then select the recipe's trigger (called CE - ClickFunnels Form Submitted) under Triggering. Every opt-in will then be sent as a conversion to Google Ads.

A Google Ads conversion tag using the recipe's trigger

What This Enables You to Do (and What It Doesn't)

When a ClickFunnels opt-in form is successfully submitted, this recipe sends an event to Google Tag Manager straight away, and any Google Ads, Google Analytics or other conversion tag tied to the trigger goes off with it.

What it does

Sends an Event to GTM

Detects every successful ClickFunnels opt-in and pushes a form submission event into your dataLayer.

Tells You Which Step Fired

Sends the ID of the funnel step with every event, so a funnel with several opt-in steps can fire different tags for different steps.

Creates a Trigger in GTM

Gives you a ready-made 'Trigger' in Google Tag Manager that you can select as the firing trigger on any conversion tag.

What it doesn't do

Send the conversion server-side

It all runs in the visitor's browser, so you're at the mercy of what their browser allows. An ad blocker can stop the Google tag from loading at all (which means nothing a visitor does gets tracked), and browser privacy rules can cut tracking down to a day (which means someone who clicks your ad on Monday and opts in on Thursday may never be tied back to that click).

No Click IDs or Identifiers

It doesn't collect the Google click ID, Facebook click ID, Meta pixel cookies, IP address or user agent, which ad platforms need to match the conversion back to a real person and the original ad they clicked.

No Personal Details

The script only fires an event. It never passes on the lead's name, email or phone number, which Google and Meta both use to match a conversion to a real person and the ad they clicked.

No Logging or Notifications

You can use Preview mode to test it works when you first set it up, but unless you go back and check every week it can break without warning (all you'll notice is 0 conversions in your ad platforms).

The Proper Way to Send Conversions to Google Ads, Meta Ads & More

If your leads only ever go to Google Analytics, this recipe does the job, because GA4 won't accept personal information or IP addresses anyway. But ad platforms are a different story. Enhanced Conversions in Google and a high Event Match Quality (EMQ) score in Meta both depend on the lead's name and email being sent server-side with the conversion. That's precisely what Converly does.

A Converly workflow sending ClickFunnels conversions to ad platforms

Here's why Converly is the best way to send conversions to Google Ads, Meta Ads, Microsoft Ads, LinkedIn Ads, etc.

Easy to set up

Converly's drag-and-drop builder makes it easy to set up your conversion tracking. Choose a trigger (such as a ClickFunnels opt-in), then add your actions (such as sending a conversion to Google Ads and Meta Ads). There's no code to write and nothing to configure by hand.

Sends Data Server-Side

Converly sends each conversion from its own servers directly to Google, Meta, ChatGPT, etc. This means it doesn't depend on what the visitor's browser lets through, so ad blockers and browser privacy settings stop getting in the way.

Sends More Data

Each conversion carries the lead's name, email and phone, plus the Google and Facebook click IDs, IP address and user agent. That's what switches on Enhanced Conversions in Google Ads and lifts your Meta Event Match Quality (EMQ) score to 9 or 10.

Full Conversion Log & Email Notifications

You can see every conversion, what was captured, what was sent to each platform, and whether it was successfully delivered or not. And if tracking ever stops, you get an email letting you know straight away.

Provides Free Support

Stuck on something? Our team has 20 years of conversion tracking and analytics experience, and provides free support to all customers. With Google Tag Manager, you are on your own (Google provides no support).

Start Your Free Trial

Start a 14-day free trial and track your conversions the proper way. No code, no complicated configuration.

About the author

Aaron Beashel
Aaron Beashel

Aaron is the founder of Converly. With over 15 years of experience in digital marketing and SaaS, he's passionate about helping businesses track and optimise their ad conversions.

Start your free trial

Easily send conversions to your ad platforms and analytics tools.
No code required.