Metaphase Marketing

Google Ads, HubSpot, Offline Conversions, Lead Generation, CRM Integration

HubSpot to Google Ads Offline Conversion Tracking: Step-by-Step

Cost per lead is one of the most widely reported metrics in Google Ads , and one of the most misleading. A campaign that generates 200 leads per month at $15 each looks excellent in the dashboard. But if the sales team closes 4% of them at an average deal size of $3,000, that math only works if the 4% are the leads Google Ads is being optimized for.

In most accounts, they are not. Google Ads optimizes for what it can see. And what it can see is the form submission , the moment the lead filled out a field on your landing page and hit submit. What happened after that, the qualification call, the demo, the proposal, the signed contract, is invisible to the algorithm. So the algorithm gets better and better at finding people who fill out forms. Not people who buy.

The fix is connecting HubSpot to Google Ads for offline conversion tracking. When a lead in your CRM hits a meaningful pipeline stage , Marketing Qualified Lead, Sales Qualified Lead, Closed Won , that event gets imported back into Google Ads tied to the original ad click. The algorithm now knows which specific keywords, audiences, and creatives produce real revenue, not just form submissions.

This guide walks through the complete setup, from enabling auto-tagging to automating conversion imports via webhook, including both the native HubSpot integration and the API-based custom approach.

HubSpot to Google Ads offline conversion pipeline showing six steps from ad click through GCLID capture to automated conversion import

Key Takeaways

  • Optimizing Google Ads for form fills trains the algorithm to find form-fillers, not buyers. Offline conversion tracking fixes this by importing real pipeline events back to the platform.

  • The GCLID (Google Click Identifier) is the thread that connects a HubSpot contact to the original Google Ads click. It must be captured on landing and stored against every CRM contact record.

  • HubSpot offers a native Google Ads integration that works for many use cases. For more control, a custom webhook pipeline gives you flexibility over which CRM stages trigger which conversion actions at what values.

  • GCLIDs expire after 90 days. For B2B businesses with long sales cycles, you need a strategy for deals that close after the expiry window.

  • Expect 10–25% CPL improvement within 60 days once meaningful offline data is flowing.

The HubSpot → Google Ads Attribution Pipeline

Before getting into individual steps, here is the full picture of how data flows through this system:

  1. Someone clicks your Google Ad. A GCLID is appended to the landing page URL.

  2. JavaScript on your landing page reads the GCLID and stores it in a cookie and a hidden form field.

  3. The prospect submits your lead form. The form data , including the GCLID , goes to your server and into HubSpot as a new Contact.

  4. The GCLID is stored as a property on the HubSpot Contact (and later, the associated Deal).

  5. The lead moves through your HubSpot pipeline. When they hit a qualifying stage (MQL, SQL, Closed Won), an automated workflow triggers a conversion import back to Google Ads.

  6. Google Ads receives the conversion with the original GCLID. Smart Bidding now knows this click led to a real outcome.

Every step is load-bearing. If the GCLID is not captured on the landing page, the whole chain breaks. If it is not stored on the HubSpot Contact, the workflow cannot access it. If the workflow is not configured correctly, the conversion never imports. Let us walk through each step.

Step 1: Enable Auto-Tagging in Google Ads

GCLIDs only appear in your URLs if auto-tagging is enabled. Navigate to Google Ads → Settings → Account Settings and confirm that auto-tagging is checked. If it is not, enable it now. Every ad click going forward will include a GCLID in the destination URL.

Step 2: Capture the GCLID on Your Landing Page

Add this JavaScript to every landing page. It reads the GCLID from the URL parameters when a visitor arrives and stores it both in a browser cookie (for returning visitors) and in any hidden form fields on the page:

function captureGCLID() {
    const params = new URLSearchParams(window.location.search);
    const gclid = params.get('gclid');

    if (gclid) {
        // 90-day cookie ,  matches Google's attribution window
        document.cookie = `gclid=${gclid}; max-age=${90 * 24 * 3600}; path=/; SameSite=Lax`;

        // Populate hidden field if present
        const field = document.getElementById('gclid_field');
        if (field) field.value = gclid;
    }
}

function getGCLID() {
    const match = document.cookie.match(/(?:^|;\s*)gclid=([^;]+)/);
    return match ? match[1] : null;
}

document.addEventListener('DOMContentLoaded', captureGCLID);
function captureGCLID() {
    const params = new URLSearchParams(window.location.search);
    const gclid = params.get('gclid');

    if (gclid) {
        // 90-day cookie ,  matches Google's attribution window
        document.cookie = `gclid=${gclid}; max-age=${90 * 24 * 3600}; path=/; SameSite=Lax`;

        // Populate hidden field if present
        const field = document.getElementById('gclid_field');
        if (field) field.value = gclid;
    }
}

function getGCLID() {
    const match = document.cookie.match(/(?:^|;\s*)gclid=([^;]+)/);
    return match ? match[1] : null;
}

document.addEventListener('DOMContentLoaded', captureGCLID);
function captureGCLID() {
    const params = new URLSearchParams(window.location.search);
    const gclid = params.get('gclid');

    if (gclid) {
        // 90-day cookie ,  matches Google's attribution window
        document.cookie = `gclid=${gclid}; max-age=${90 * 24 * 3600}; path=/; SameSite=Lax`;

        // Populate hidden field if present
        const field = document.getElementById('gclid_field');
        if (field) field.value = gclid;
    }
}

function getGCLID() {
    const match = document.cookie.match(/(?:^|;\s*)gclid=([^;]+)/);
    return match ? match[1] : null;
}

document.addEventListener('DOMContentLoaded', captureGCLID);

The cookie expiry of 90 days is intentional. Google's attribution window goes up to 90 days, so a lead who clicked your ad 60 days ago and converts today still needs a valid GCLID in the cookie to be attributed correctly.

Step 3: Add a Hidden GCLID Field to Your HubSpot Forms

In HubSpot's form editor, add a hidden field to every form that should be tracked. Name the field gclid (this will map to the HubSpot property you create in the next step).

If you are using a custom form outside of HubSpot's native form builder, add this HTML field and populate it from the cookie on page load:

<form>
  <input type="text" name="firstname" placeholder="First Name">
  <input type="email" name="email" placeholder="Email">
  <input type="tel" name="phone" placeholder="Phone">

  <!-- GCLID hidden field -->
  <input type="hidden" id="gclid_field" name="gclid" value="">

  <button type="submit">Get Started</button>
</form>

<script>
  // Read GCLID from cookie and populate hidden field
  function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
    return '';
  }
  document.getElementById('gclid_field').value = getCookie('gclid');
</script>
<form>
  <input type="text" name="firstname" placeholder="First Name">
  <input type="email" name="email" placeholder="Email">
  <input type="tel" name="phone" placeholder="Phone">

  <!-- GCLID hidden field -->
  <input type="hidden" id="gclid_field" name="gclid" value="">

  <button type="submit">Get Started</button>
</form>

<script>
  // Read GCLID from cookie and populate hidden field
  function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
    return '';
  }
  document.getElementById('gclid_field').value = getCookie('gclid');
</script>
<form>
  <input type="text" name="firstname" placeholder="First Name">
  <input type="email" name="email" placeholder="Email">
  <input type="tel" name="phone" placeholder="Phone">

  <!-- GCLID hidden field -->
  <input type="hidden" id="gclid_field" name="gclid" value="">

  <button type="submit">Get Started</button>
</form>

<script>
  // Read GCLID from cookie and populate hidden field
  function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
    return '';
  }
  document.getElementById('gclid_field').value = getCookie('gclid');
</script>

Step 4: Map the GCLID to a HubSpot Contact Property

Create a custom Contact property in HubSpot to store the GCLID:

  1. Go to HubSpot → Settings → Properties → Contact Properties → Create property.

  2. Label: "Google Click ID" (or similar).

  3. Internal name: google_click_id.

  4. Field type: Single-line text.

  5. Save.

Map your form's gclid hidden field to this property so that when a form is submitted, the GCLID is stored on the Contact record automatically. In HubSpot's native form builder, this is done in the form settings under field mapping. For custom forms integrated via the HubSpot Forms API, pass the property name in your payload.

If you are tracking deal-stage conversions (which you should be), also create the same property on the Deal object and create a workflow to copy it from the Contact when a Deal is created.

HubSpot stage to Google Ads conversion mapping table showing MQL, Meeting Booked, Proposal Sent, and Closed Won with suggested values and bidding impact

Step 5: Create a Google Ads Conversion Action

In Google Ads, create the conversion action that HubSpot will import into:

  1. Go to Google Ads → Tools → Conversions → New Conversion Action.

  2. Select Import → CRMs, files, or other data sources → Track conversions from clicks.

  3. Name it to match the HubSpot pipeline stage (e.g., "Marketing Qualified Lead," "Booked Call," "Closed Won").

  4. Set Value to either a fixed estimate per stage or import dynamically from HubSpot deal amounts.

  5. Set Count to "One."

  6. Set Attribution model to Data-driven if eligible.

Note the exact conversion action name , you will need it in your webhook or HubSpot workflow configuration.

Step 6, Option A: Use the Native HubSpot Google Ads Integration

For straightforward use cases, HubSpot's built-in Google Ads integration handles GCLID capture and import automatically:

  1. In HubSpot, go to Settings → Marketing → Ad Tracking.

  2. Connect your Google Ads account via OAuth.

  3. Under "Ads tracking," enable contact and company ad attribution.

  4. In the Revenue Attribution settings, define which lifecycle stage or deal stage triggers a conversion import and which Google Ads conversion action it maps to.

HubSpot automatically reads the GCLID from the hs_google_click_id property on the Contact (which HubSpot populates automatically when ad tracking is enabled and the user arrived from a Google Ad). When the Contact reaches the defined stage, HubSpot imports the conversion.

Limitations of the native integration: It works at the Contact level, not the Deal level. It does not support dynamic conversion values from Deal amounts out of the box. If you need deal-stage granularity or dynamic revenue values, Option B gives you full control.

Step 6, Option B: Custom Webhook → Google Ads API

For more control over which stages trigger which conversion actions and at what values, build a HubSpot workflow that fires a webhook to a custom endpoint, which then uploads the conversion via the Google Ads API:

HubSpot workflow:

  1. Create a Deal-based workflow (not Contact-based, so you have access to deal properties and amounts).

  2. Set the enrollment trigger to "Deal Stage is Closed Won" (or whichever stage matters).

  3. Add action: Send a webhook to your endpoint URL.

  4. Include the following deal properties in the webhook payload: gclid, email, deal_amount, close_date.

Webhook endpoint (Python):

from datetime import datetime

def handle_hubspot_stage_change(payload: dict):
    gclid = payload["properties"].get("google_click_id", "")
    if not gclid:
        # No GCLID ,  lead did not come from Google Ads, skip import
        return

    deal_amount = float(payload["properties"].get("amount", 500))
    close_date = payload["properties"].get("closedate", datetime.utcnow().isoformat())

    upload_offline_conversion(
        customer_id=GOOGLE_ADS_CUSTOMER_ID,
        conversion_action_id=CLOSED_WON_ACTION_ID,
        gclid=gclid,
        conversion_value=deal_amount,
        conversion_time=close_date
    )
from datetime import datetime

def handle_hubspot_stage_change(payload: dict):
    gclid = payload["properties"].get("google_click_id", "")
    if not gclid:
        # No GCLID ,  lead did not come from Google Ads, skip import
        return

    deal_amount = float(payload["properties"].get("amount", 500))
    close_date = payload["properties"].get("closedate", datetime.utcnow().isoformat())

    upload_offline_conversion(
        customer_id=GOOGLE_ADS_CUSTOMER_ID,
        conversion_action_id=CLOSED_WON_ACTION_ID,
        gclid=gclid,
        conversion_value=deal_amount,
        conversion_time=close_date
    )
from datetime import datetime

def handle_hubspot_stage_change(payload: dict):
    gclid = payload["properties"].get("google_click_id", "")
    if not gclid:
        # No GCLID ,  lead did not come from Google Ads, skip import
        return

    deal_amount = float(payload["properties"].get("amount", 500))
    close_date = payload["properties"].get("closedate", datetime.utcnow().isoformat())

    upload_offline_conversion(
        customer_id=GOOGLE_ADS_CUSTOMER_ID,
        conversion_action_id=CLOSED_WON_ACTION_ID,
        gclid=gclid,
        conversion_value=deal_amount,
        conversion_time=close_date
    )

This approach lets you pass the actual deal amount as the conversion value, giving Google Ads real revenue data instead of a flat estimate per lead. Target ROAS bidding becomes dramatically more accurate when it is working with real dollar amounts per conversion.

Conversion Event Mapping

You do not need to import every pipeline stage as a conversion. Import the stages that best represent real business value. A typical B2B mapping looks like this:

HubSpot Stage

Google Ads Conversion Action

Value

Marketing Qualified Lead (MQL)

"Qualified Lead"

Fixed: $300

Meeting Booked

"Booked Call"

Fixed: $500

Proposal Sent

Import if high-volume only

Fixed: $750

Closed Won

"Closed Won"

Dynamic: deal amount

Import more stages if your sales cycle is long and you want the algorithm to optimize earlier in the funnel. But always include Closed Won with the actual deal value , that is the signal that transforms bidding quality.

The GCLID 90-Day Expiry Problem

GCLIDs have a 90-day attribution window. If your typical B2B deal closes in 120 days, a significant portion of your Closed Won conversions will not have a valid GCLID to import.

Strategies for handling this:

  • Import earlier-stage conversions: Import MQL or Booked Meeting events, which typically happen within the 90-day window even for long-cycle deals. These give the algorithm a usable signal even if the final close is outside the window.

  • Enhanced Conversions for Leads: Enable this in Google Ads and send hashed email and phone data. Google uses probabilistic matching to attribute conversions even without a valid GCLID, recovering some of the conversions that would otherwise be lost.

  • CRM data for audience building: Upload your Closed Won contact list as a customer match audience in Google Ads. This helps the algorithm understand what buyers look like even if individual conversions cannot be directly attributed.

Troubleshooting

GCLID not being captured:

  • Check that auto-tagging is enabled in Google Ads account settings.

  • Verify the JavaScript is loading before any form submission code.

  • Test by clicking a live Google Ad and checking the URL for the gclid parameter.

  • Check whether your landing page redirects before the JavaScript can capture the parameter , redirects can strip the GCLID.

Conversions not appearing in Google Ads:

  • Verify the conversion action name in your import matches exactly what appears in Google Ads (case-sensitive).

  • Check that the conversion time is within 90 days of the click time.

  • Look for "Import Failed" status vs. "Imported" in the Conversions UI.

Match rate below 30%:

  • Your GCLID capture is likely inconsistent , some forms or pages are not capturing it.

  • Enable Enhanced Conversions for Leads to recover non-GCLID conversions via hashed identity matching.

  • Check whether forms are embedded in iframes, which can interfere with cookie access.

Bidding Strategy Once Data Is Flowing

Once you have 30 or more offline conversions per month flowing through the pipeline, you can begin shifting to smarter bidding strategies:

  • Target CPA: Set to 1.2–1.5x your current offline cost per qualified lead. Let it tighten over 30 days as the algorithm calibrates.

  • Target ROAS: Use only when you are importing dynamic deal values (actual Closed Won amounts). Once you have consistent revenue data, this is the most powerful setting available.

Expect CPL to temporarily increase in weeks three and four as junk leads are deprioritized. This is correct behavior. By month two, CPL will stabilize lower against a significantly higher close rate, because the algorithm is buying better traffic.

Want This Set Up for Your Account?

Metaphase builds complete HubSpot-to-Google Ads attribution pipelines , from GCLID capture through automated conversion imports. Most implementations are live within five business days.

See our Google Ads approach →

Book a consultation

Available

Metaphase Marketing

Working Hours ( CST )

8am to 8pm

Available

Metaphase Marketing

Working Hours ( CST )

8am to 8pm

👇 Have a question? Ask below 👇

👇 Have a question? Ask below 👇

METAPHASE MARKETING

X Logo
Instagram Logo
Linkedin Logo

Let’s work together

© 2024 Metaphase Marketing. All rights reserved.

METAPHASE MARKETING


X Logo
Instagram Logo
Linkedin Logo

Let’s work together

© 2024 Metaphase Marketing. All rights reserved.

METAPHASE MARKETING

X Logo
Instagram Logo
Linkedin Logo

Let’s work together

© 2024 Metaphase Marketing. All rights reserved.