How to implement Enhanced Conversions For Web using Google Ads API

Introduction

Enhanced Conversions in Google Ads function as an upgrade to the traditional method of tracking the effectiveness of advertisements. Typically, conversions – actions of value such as purchases or sign-ups – are tracked using cookies. However, with the increasing use of multiple devices by individuals and growing privacy concerns, this method faces limitations. Enhanced Conversions address these challenges by utilizing additional, privacy-safe data (like hashed email addresses) to improve the accuracy of conversion tracking. This approach enables a clearer understanding of the advertisement’s performance by tying user actions more directly to your ads. The benefit is twofold: it allows for more precise measurement of ad effectiveness and supports the optimization of advertising strategies, ensuring that the advertising budget is invested wisely.

If Enhanced Conversions are unfamiliar to you, I recommend checking out my prior [article][https://www.linkedin.com/pulse/mastering-enhanced-conversions-marketers-guide-world-khissamiyev-gm9gc/?trackingId=hFZ3R7uXRz%2BCJbWwBSnGlw%3D%3D]. That piece delves into what Enhanced Conversion entails and outlines how to set it up with Google Tag Manager, providing a potentially easier approach for implementation. Using the implementation instructions provided there could simplify the process for those new to Enhanced Conversions. Conversely, implementing Enhanced Conversions (EC) through the Google Ads API is a more involved process. This method might be preferred if other options do not meet your needs or if you seek the capability to send first-party conversion data not at the moment of conversion but within 24 hours after it has occurred. This flexibility allows you to query your databases or access your CRM system to gather the necessary conversion data, ensuring a more comprehensive and accurate data collection process for your advertising performance analysis. If you new to Google Ads API, I recommend to follow my article on how to set it up for the first time.

High-Level Overview

That being said, let’s have a look at how a typical implementation of Google Ads via API might look, using a made-up story about Elly and her quest to find the ideal guitar for her friend Joel.

  • First Step: Elly comes across an ad for a Fender Stratocaster on her phone while watching videos on YouTube and taps on it. This ad is part of an advertising campaign by LastOfGuitars.com on Google Ads.
  • Making the Purchase: Later, using her tablet, Elly buys the guitar. Despite switching devices, this purchase is the key action that LastOfGuitar.com aims to monitor.
  • Sending Purchase Info: After Elly finishes buying, the website of LastOfGuitar.com quickly sends details of her purchase, including a unique order ID & GCLID, to Google Ads.
  • Securing the Data: An API connection set up by LastOfGuitar.com then anonymizes Elly’s personal information from the purchase data and forwards it securely to Google Ads. This has to be done within a day.
  • Matching and Reporting: Google Ads takes Elly’s anonymized information, looks for a match in their user database, and when found, the purchase is recorded in LastOfGuitar.com’s Google Ads account. This process offers them insights into how well their ads are performing.

Prerequisites

Now that we’ve seen what the process looks like, let’s discuss the essential steps required to make this happen. To set up Enhanced Conversions (EC) in Google Ads using the API, there are a few prerequisites we need to address.

What you need to have:
  • All prerequsites related to settting up Google Ads API: Please check my article for this. In short you need Standard Google Ads Account, Manager Google Ads Account (MCC), Developer Token with at least Basic Access, Client ID, Client Secret, and Refresh Token.
  • Conversion Tags: Ensure that your website, where conversions are tracked, is either already equipped with conversion tags or that you have the capability to create them.
  • Conversion Linker: Ensure the Conversion Linker is active to track clicks accurately, capturing identifiers like GCLID.

Usage note for the code examples

This article will present a series of coding examples that are suited for our basic guide and perform well under these conditions. However, for use in production environments, you should consult the official documentation of the Google Ads API regarding Enhanced Conversions and adapt the code according to your requirements. Ultimately, it is crucial to carry out your own thorough research and tailor solutions that fit your specific needs, treating this article merely as an illustration of potential implementations, not as mandatory guidance.

STEP I. Initial setup

Before launching Enhanced Conversions for Web, we need to conduct some preparatory tasks. We need to define our Conversion Customer in Google Ads. Then, we need to check whether a conversion action of type WEBPAGE exists and it is enabled in your Conversion Customer in Google Ads.

1. Determine Conversion Customer in Google Ads

The Google Ads Conversion Customer is the Google Ads account responsible for creating and managing conversions for the client. This could either be a Google Ads Standard Account or a Google Ads Manager account, especially if you use cross-account conversion tracking.

This article presupposes that you have implemented the instructions from my earlier tutorial on setting up the Google Ads API and that you have installed the nano editor. Recall that we created a ‘Google Ads API’ folder on the Desktop and accessed it using Terminal. Inside, we initiated a virtual environment named ‘googleAdsApi’ using Python’s venv package, where we executed our operations, including the installation of the google-ads library and the google-ads-python repository. To resume your work within this virtual environment, open Terminal at the ‘Google Ads API’ folder and input the command below to reactivate the virtual environment previously established. If your setup for the Google Ads API was configured differently, please adjust accordingly.

source googleAdsApi/bin/activate

Next, create a folder named “EC” within the “Google Ads API” directory. Then, proceed to create a Python file called “get_conversion_customer.py”. To accomplish this, enter the following command in the terminal:

mkdir EC
touch EC/get_conversion_customer.py
nano EC/get_conversion_customer.py

In the file, enter the following code. Remember to substitute {path} with the full path to your google-ads.yaml file, which we discussed creating in my previous article.

import os
import argparse
import sys
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

# Set the path to the google-ads.yaml file
os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

def main(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    query = """
        SELECT
          customer.conversion_tracking_setting.google_ads_conversion_customer
        FROM customer
        """

    # Issues a search request using streaming.
    response = ga_service.search_stream(customer_id=customer_id, query=query)

    for batch in response:
        for row in batch.results:
            print(
                f"Conversion Customer: {row.customer.conversion_tracking_setting.google_ads_conversion_customer}"
            )

if __name__ == "__main__":
    # Load the GoogleAdsClient from the specified configuration file.
    googleads_client = GoogleAdsClient.load_from_storage()

    parser = argparse.ArgumentParser(
        description="Get the conversion customer ID for specified customer."
    )

    # Argument for specifying the customer ID via command-line.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID to retrieve the conversion customer."
    )

    args = parser.parse_args()

    try:
        main(googleads_client, args.customer_id)
    except GoogleAdsException as ex:
        print(
            f'Request with ID "{ex.request_id}" failed with status '
            f'"{ex.error.code().name}" and includes the following errors:'
        )
        for error in ex.failure.errors:
            print(f'\tError with message "{error.message}".')
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print(f"\t\tOn field: {field_path_element.field_name}")
        sys.exit(1)

Save your changes by pressing Ctrl+O, confirm the filename, and then press Ctrl+X to exit the editor. Next, execute the following commands in Terminal to retrieve your Conversion Customer. Remember to supply the Customer ID, which is the specific identifier for the Google Ads Standard Account from which you want to extract data.

python3 EC/get_conversion_customer.py -c {customer_id}

The code’s output will yield one of two results: it will either display the Customer ID of a Standard Google Ads Account, indicating that this specific account is independently managing its conversions, or it will show the Customer ID of a Google Ads Manager Account (MCC). If an MCC ID is displayed, this signifies that the MCC is handling conversion tracking for potentially several client accounts under its supervision. Please make sure to securely store this information, as it will be required for future use.

2. Confirm if a conversion action of the type WEBPAGE is present and enabled

Now that our Conversion Customer has been identified, we can proceed to check whether a WEBPAGE type conversion action is present and active. We’ll follow the same steps as before:

In your “Google Ads API” directory, use the above code in Terminal to create a new Python file named “checking_conversion_action_exists_enabled.py”:

touch EC/checking_conversion_action_exists_enabled.py
nano EC/checking_conversion_action_exists_enabled.py

Next, fill the file with the provided code. Be sure to replace {path} with the actual path to your google-ads.yaml file.

import os
import argparse
import sys
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

# Set the path to the google-ads.yaml file
os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

def get_conversion_tracking_status(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    query = """
        SELECT
          customer.conversion_tracking_setting.conversion_tracking_status
        FROM customer
        """

    response = ga_service.search_stream(customer_id=customer_id, query=query)

    for batch in response:
        for row in batch.results:
            status_code = row.customer.conversion_tracking_setting.conversion_tracking_status

            status_mapping = {
                0: "UNSPECIFIED",
                1: "UNKNOWN",
                2: "NOT_CONVERSION_TRACKED",
                3: "CONVERSION_TRACKING_MANAGED_BY_SELF",
                4: "CONVERSION_TRACKING_MANAGED_BY_THIS_MANAGER",
                5: "CONVERSION_TRACKING_MANAGED_BY_ANOTHER_MANAGER"
            }

            status_name = status_mapping.get(status_code, "Unexpected Status Code")

            print(f"Conversion Tracking Status for Customer {customer_id}: {status_name}") 

if __name__ == "__main__":
    # Load the GoogleAdsClient from the specified configuration file.
    googleads_client = GoogleAdsClient.load_from_storage()

    parser = argparse.ArgumentParser(
        description="Get the conversion tracking status for a specified customer."
    )

    # Argument for specifying the customer ID via command-line.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID for which to retrieve the conversion tracking status."
    )

    args = parser.parse_args()

    try:
        get_conversion_tracking_status(googleads_client, args.customer_id)
    except GoogleAdsException as ex:
        print(f"Error: {ex.error.code().name} - {ex.message}")

Save the changes to the file and exit the editor. Afterwards, run the specified commands in Terminal to determine if the conversion action is present and active. Make sure to enter the Customer ID, which is the unique identifier for the specific Google Ads Standard Account you are querying.

python3 EC/checking_conversion_action_exists_enabled.py -c {customer_id}

The code will output a status describing the conversion tracking setup for your account. Here’s what each status means:

  • UNSPECIFIED (0), UNKNOWN (1), NOT_CONVERSION_TRACKED (2): Conversion tracking is not set up. Proceed to “(Optional) Set up a conversion action of the type WEBPAGE in your Conversion Customer”.
  • CONVERSION_TRACKING_MANAGED_BY_SELF (3), CONVERSION_TRACKING_MANAGED_BY_THIS_MANAGER (4), CONVERSION_TRACKING_MANAGED_BY_ANOTHER_MANAGER (5): Conversion tracking exists and is enabled. You can proceed to “3. Check if you have accepted the customer data terms”.

2.1 (Optional) Set up a conversion action of the type WEBPAGE in your Conversion Customer

If your Conversion Customer doesn’t have a WEBPAGE conversion action, you can create one using the Google Ads UI, which is generally easier than doing it programmatically through the API:

Configuring Conversion Actions via Google Ads UI: Access your Google Ads account, click on the “+” button, and select “Conversion Action.” Opt for “Website” from the available choices. Enter your website domain and determine the conversion method, choosing either by URL (the straightforward method) or through a code snippet (the more complex approach). Complete the necessary forms based on your specific requirements.

(Optional) Check existing Conversion Actions and Record Conversion ID

To send Enhanced Conversion data to the Google Ads API, it’s essential to identify the conversion_action_id. By following the steps outlined below, the output will appear in the format customers/{customer_id}/conversionActions/{conversion_action_id}. Make sure to note the conversion_action_id associated with the conversion action you aim to enhance. You can refer to this conversion_action_id later as a reference for verifying your own implementation.

In your “Google Ads API” directory, use the above code in Terminal to create a new Python file named “check_existing_conversion_actions.py”:

touch EC/check_existing_conversion_actions.py
nano EC/check_existing_conversion_actions.py

Next, populate the file with the given code. Remember to substitute {path} with the correct path to your google-ads.yaml configuration file. Additionally, adjust the code as needed to suit your specific conversion objectives.

import os
import argparse
import sys
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

# Set the path to the google-ads.yaml file
os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

def get_web_conversions(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    # SQL-like query to fetch webpage conversions
    query = """
        SELECT
          conversion_action.resource_name,
          conversion_action.name,
          conversion_action.status
        FROM conversion_action
        WHERE conversion_action.type = 'WEBPAGE'
        	AND conversion_action.status = 'ENABLED'
        """

    response = ga_service.search_stream(customer_id=customer_id, query=query)

    # Iterating through streamed response batches
    for batch in response:
        for row in batch.results:
            resource_name = row.conversion_action.resource_name
            name = row.conversion_action.name
            status = row.conversion_action.status

            # Map the numerical status to more understandable text
            status_mapping = {
                'ENABLED': "Enabled",
                'PAUSED': "Paused",
                'REMOVED': "Removed"
            }

            status_description = status_mapping.get(status, "Unknown Status")
            print(f"Conversion Name: {name}, Resource Name: {resource_name}, Status: {status_description}")

if __name__ == "__main__":
    # Load the GoogleAdsClient from the specified configuration file.
    googleads_client = GoogleAdsClient.load_from_storage()

    parser = argparse.ArgumentParser(
        description="Retrieve web conversion actions for a specific customer."
    )

    # Argument for specifying the customer ID via command-line.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID for which to retrieve the web conversions."
    )

    args = parser.parse_args()

    try:
        get_web_conversions(googleads_client, args.customer_id)
    except GoogleAdsException as ex:
        print(f"Error: {ex.error.code().name} - {ex.message}")

Save the changes to the file and exit the editor. Afterwards, run the specified commands in Terminal to determine if the conversion action is present and active. Make sure to enter the Customer ID, which is the unique identifier for the specific Google Ads Standard Account you are querying.

python3 EC/check_existing_conversion_actions.py -c {customer_id}

The output should be a list of webpage-type conversion actions whose status is configured as ‘Enabled.’

3. Check if you have accepted the customer data terms

We can programmatically find out whether we accepted the customer data terms. To do so, we follow the same drill as above. First let’s create file “check_data_terms_acceptance.py”.

touch EC/check_data_terms_acceptance.py
nano EC/check_data_terms_acceptance.py

Next, populate the file with the given code. Remember to substitute {path} with the correct path to your google-ads.yaml configuration file.

import os
import argparse
import sys
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

# Set the path to the google-ads.yaml file
os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

def get_customer_data_terms_status(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    # SQL-like query to check if customer has accepted data terms
    query = """
        SELECT
          customer.id,
          customer.conversion_tracking_setting.accepted_customer_data_terms
        FROM customer
        """

    response = ga_service.search_stream(customer_id=customer_id, query=query)

    # Iterating through streamed response batches
    for batch in response:
        for row in batch.results:
            customer_id = row.customer.id
            accepted_data_terms = row.customer.conversion_tracking_setting.accepted_customer_data_terms

            # Map the boolean status to a more understandable text
            accepted_status = "Yes" if accepted_data_terms else "No"
            print(f"Customer ID: {customer_id}, Accepted Customer Data Terms: {accepted_status}")

if __name__ == "__main__":
    # Load the GoogleAdsClient from the specified configuration file.
    googleads_client = GoogleAdsClient.load_from_storage()

    parser = argparse.ArgumentParser(
        description="Check if the customer has accepted the data terms."
    )

    # Argument for specifying the customer ID via command-line.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID to check for data terms acceptance."
    )

    args = parser.parse_args()

    try:
        get_customer_data_terms_status(googleads_client, args.customer_id)
    except GoogleAdsException as ex:
        print(f"Error: {ex.error.code().name} - {ex.message}")

Save the changes to the file and exit the editor. Afterwards, run the specified commands in Terminal to determine if the conversion action is present and active. Make sure to enter the Customer ID, which is the unique identifier for the specific Google Ads Standard Account you are querying.

python3 EC/check_data_terms_acceptance.py -c {customer_id}

This will display whether a particular Standard Google Ads Account has agreed to the customer data terms.

3.1 (Optional) Accept the customer data terms

For the purposes of this guide, it’s assumed that you are using an MCC to manage Enhanced Conversions for related Standard Google Ads accounts. Should your setup differ, you’ll need to activate Enhanced Conversions for each individual Standard Google Ads Account separately. In your MCC account for Google Ads, navigate to “Goals,” then proceed to “Settings” and select “Enhanced Conversions.” Activate Enhanced Conversions by clicking on “Turn on enhanced conversions.” You are then required to read and accept the compliance statement which confirms your adherence to policies and acknowledges that the Google Ads Data Processing Terms are applicable to your usage of Enhanced Conversions. Confirm your agreement by selecting Agree. Following this, choose “Google Ads API” under “Which method should I use”, and finalize your settings by clicking “Save.”

Please note, that while you can use different methods such as Google Tag Manager or Gtag for some conversions and Google Ads API for others, ensure that the method selected matches your implementation. If you set the method to use the API for sending conversion data, ensure it is correctly set to API; an incorrect setting may cause errors during process execution.

4. Setting up a Conversion Tracking Tag on your Website

For the purposes of this article, let’s assume you are utilizing Google Tag Manager (GTM) to manage tags on your website. Note that if you’re using gtag.js directly, some parts of these instructions may need to be adjusted accordingly. Ensure that the GTM code snippets are installed across all your website pages.

Adjustments for Existing Setups

If you have previously configured tags and they are effectively transmitting necessary fields such as Conversion ID, Conversion Label, and GCLID, your setup might not require any significant adjustments—just ensure these tags are functioning correctly. However, it’s crucial to check if these tags also transmit an Order ID, particularly if tracking detailed conversion actions, like purchases. If your tags don’t currently include this data, update them to incorporate the Order ID field.

Setting Up Conversion Linker

An essential step is ensuring that a Conversion Linker tag is configured in GTM to fire on all pages. This configuration is crucial for effectively capturing the Google Click Identifier (GCLID), which links user actions directly to your ads and facilitates accurate conversion tracking.

To set up a Conversion Linker tag in Google Tag Manager (GTM):

  1. Log into your GTM account and navigate to the appropriate container.
  2. Create a new tag and select “Conversion Linker” as the tag type from the tag configuration options.
  3. Set the trigger to “All Pages” to ensure the tag fires with each page visit, which captures and retains the GCLID across user sessions.
  4. Save the tag with a descriptive name like “All Pages - Conversion Linker” for easy recognition.

Setting Up Conversion Tracking

For each specific conversion action (e.g., form submissions or transactions), create a separate Google Ads Conversion Tracking tag within GTM. Important data to include in each of these tags are:

  • Conversion ID and Conversion Label: Link the captured data directly to the correct conversion action in your Google Ads account by inputting the respective identifiers provided by Google Ads.
  • Transaction ID (e.g., Order ID): Include this identifier to distinguish individual transactions or conversion events, enhancing the accuracy of your tracking.

Regarding Transaction ID (Order ID)

Please note that the creation of the Order ID is typically managed on the client side, and it’s crucial from an implementation perspective to ensure this information can be captured and attached to the Google Ads conversion tag effectively. One efficient method is to have developers push the Order ID into the data layer. Subsequently, in Google Tag Manager (GTM), you can create a data layer variable to capture this Order ID. Then, configure the Google Ads conversion tag in GTM to include a field specifically for the Order ID, referencing this newly created variable. It is paramount to adhere to Google’s guidelines regarding transaction IDs: they can include numbers, letters, and special characters like dashes or spaces, with a character limit of 64 characters, and must be unique for each transaction. Importantly, the transaction IDs must not include any information that could be used to identify individual customers.

Triggers and Testing

After integrating the necessary fields into each tag, establish the appropriate triggers. These triggers define when and how the tags should activate, such as on clicks, form submissions, or page views indicative of a conversion event.

Validation

Before finalizing your GTM setup, conduct thorough tests to ensure that all tags are firing correctly and at the right moments. Use GTM’s Preview mode or tools like Google Tag Assistant to ensure tags are not only firing but also sending all required data accurately. This includes the Conversion ID, Conversion Label, GCLID, and Order ID.

Important: Coordinating Data for Enhanced Conversions

For effective implementation of Enhanced Conversions using Google Ads API, precise synchronization of data between your website’s interface and backend is essential:

  1. Front-End Data Capture: During a user’s conversion event, the Google Ads Conversion Tag should automatically gather and transmit essential information such as the Order ID and Conversion ID plus Conversion Label directly to Google Ads. In addition to this, I recommend putting Conversion Action Name into a separate field.
  2. Backend Integration: Concurrently, ensure an API call is made from your site to your backend, sending the Transaction ID (Order ID) and attaching the Conversion Action Name for record-keeping. The reason for doing that will be evident in the next part.

Step II. Main Setup

Once we’ve confirmed that all prerequisites are in place, we can begin the actual implementation of Enhanced Conversions for Web using the Google Ads API.

1. Normalize and hash user data

I suggest dividing the task into two distinct functions: one for normalizing and hashing emails, and another for different types of data. For the purposes of this beginner’s guide, everything will be done locally on your machine. However, in a production setting, these processes would be integrated into your backend. It’s crucial to hash personal identifiers such as emails, phone numbers, first names, last names, and street addresses. Conversely, you should not hash country codes, state codes, cities, or zip codes. I recommend hashing this information in advance to ensure it’s correctly normalized and hashed before attempting to create a ConversionAdjustment object. It’s critical to perform this step accurately, as mistakes can prevent Google Ads from properly matching the uploaded data, which could result in a reduction in reported conversions.

Alright, I’ll assume your conversion records are stored in a JSON file, organized as a list with each element being an object. To demonstrate, let’s create a JSON file, then proceed to normalize and hash its contents:

touch EC/sample_records.json
nano EC/sample_records.json

Please enter the following text, save the file, and then close it:

[
    {
        "email": "[email protected]",
        "first_name": "Joel",
        "last_name": "Miller",
        "country_code": "US",
        "postal_code": "98101",
        "phone": "+1 206 5550123",
        "order_id": "TLOU2023COLL",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-09-22 14:30:00-07:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
    },
    {
        "email": "[email protected]",
        "first_name": "Ellie",
        "last_name": "Williams",
        "country_code": "US",
        "postal_code": "83001",
        "phone": "+1 307 5550124",
        "order_id": "TLOU2023ELL",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-10-05 16:45:00-06:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
    },
    {
        "email": "[email protected]",
        "first_name": "Tommy",
        "last_name": "Miller",
        "country_code": "US",
        "postal_code": "83001",
        "phone": "+1 307 5550125",
        "order_id": "TLOU2023GUIDE",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-10-10 12:00:00-06:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (Linux; Android 10; SM-A505FN) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36"
    }
]

Next, we’ll create a Python script that will handle the normalization and hashing for us:

touch EC/normalization_and_hashing.py
nano EC/normalization_and_hashing.py

Enter the code provided below, save the file, and then close it:

import json
import hashlib
import argparse
import datetime
import re
import os

def normalize_and_hash_email(email):
    """Normalizes and hashes email addresses specifically."""
    email = email.lower()
    if '@gmail.com' in email or '@googlemail.com' in email:
        local_part, domain = email.split('@')
        email = local_part.replace('.', '') + '@' + domain
    return hashlib.sha256(email.encode()).hexdigest()

def normalize_and_hash(s):
    """Normalizes and hashes general strings using SHA-256."""
    normalized_string = s.strip().lower()
    return hashlib.sha256(normalized_string.encode()).hexdigest()

def hash_sensitive_data(input_path, output_directory=None):
    """Read JSON file, hash sensitive data, and output to a new file."""
    fields_to_hash = {'first_name', 'last_name', 'phone'}
    with open(input_path, 'r') as file:
        data = json.load(file)

    for record in data:
        # Handle email separately
        if 'email' in record:
            record['email'] = normalize_and_hash_email(record['email'])
        # Handle other fields
        for key in fields_to_hash:
            if key in record:
                record[key] = normalize_and_hash(record[key])

    # Prepare output path
    directory = output_directory if output_directory else os.path.dirname(input_path)
    timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    new_file_name = f"{timestamp}_hashed_normalized.json"
    new_file_path = os.path.join(directory, new_file_name)

    with open(new_file_path, 'w') as file:
        json.dump(data, file, indent=4)

    print(f"Processed data saved to {new_file_path}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Hash sensitive data in a JSON file.")
    parser.add_argument("input_path", help="The path to the input JSON file.")
    parser.add_argument("--output_path", nargs='?', default=None, help="Optional: The directory path where the output file will be saved.")
    args = parser.parse_args()

    hash_sensitive_data(args.input_path, args.output_path)

Now we are ready to test our function. Please ensure you replace the placeholder path with the path to your JSON file and specify where you want to save the output. If the second argument is not provided, the file will be saved in the same directory as the script.

python3 EC/normalization_and_hashing.py {path} {path2}

Ideally, the resultant JSON should display the following output:

[
    {
        "email": "01642a1707ba2c5bf4cb5af0e47891445a92a5eb225a0d72dc19246fede94c6a",
        "first_name": "a6761ccff1191f3ee53acada4f7965241538511ef6eb52d37974507ab5a9023e",
        "last_name": "d2641888ed6426afd3d3649066cf3614ec2eb63d3ec90ba2e3a54ba2dffa61ca",
        "country_code": "US",
        "postal_code": "98101",
        "phone": "97526cd14d2dd44a6f95ec8169513ea6f8bafd14dbb56d8bf3d61f66f0325566",
        "order_id": "TLOU2023COLL",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-09-22 14:30:00-07:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
    },
    {
        "email": "6a7675f3dbb6cf24f44367f16693b17ed15fa9db2ad40a19df0cdb8d495adf92",
        "first_name": "027434cce1114811be52fa56af57a6550bda1c7777be20f4e51f4a6952574c72",
        "last_name": "d17108747e922613fe66bf5b0ec450801f513b92e91d7838f0b739df9f68ba98",
        "country_code": "US",
        "postal_code": "83001",
        "phone": "7a3be50294dae6fb0f1c040f50a043ff6161a6ec2fb9e15e35999e0b5d6b1f61",
        "order_id": "TLOU2023ELL",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-10-05 16:45:00-06:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
    },
    {
        "email": "2559409b1a4bd760e4ddf8513ad3ea22f8e1b9aba8123a946246449ac46b6072",
        "first_name": "044f4b3501cd8e8131d40c057893f4fdff66bf4032ecae159e0c892a28cf6c8e",
        "last_name": "d2641888ed6426afd3d3649066cf3614ec2eb63d3ec90ba2e3a54ba2dffa61ca",
        "country_code": "US",
        "postal_code": "83001",
        "phone": "7b8bd972e97f2eca78d73d617d32c05724e68cd666ee15cda07c1d1082f042cd",
        "order_id": "TLOU2023GUIDE",
        "conversion_action_name": "TDY",
        "conversion_date_time": "2023-10-10 12:00:00-06:00",
        "currency_code": "USD",
        "user_agent": "Mozilla/5.0 (Linux; Android 10; SM-A505FN) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36"
    }
]

1.1 Get conversion_action_id from Google Ads API using Conversion Action Name

Previously, I recommended making an API call to your backend when a user conversion occurs, including the Conversion Action Name obtained from Google Ads. However, a challenge emerges because to make an API call to the Google Ads API, the Conversion Action Name is insufficient; instead, you need the conversion_action_id. While it’s possible to manually retrieve this information using the queries discussed earlier in the article, this method lacks scalability. For efficient operation, we must devise a strategy to automatically attach the conversion_action_id to each of our records.

First, let’s construct a Python file:

touch EC/inserting_conversion_action_id.py
nano EC/inserting_conversion_action_id.py

After opening the terminal, enter this code, save the changes, and exit the nano editor. Remember to substitute {path} with the correct path to your google-ads.yaml configuration file.

import json
import argparse
import os
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

def load_json_records(json_path):
    """
    Loads JSON records from a file.
    Args:
        json_path (str): Path to the JSON file.
    Returns:
        list: A list of dictionaries each representing a record.
    """
    with open(json_path, 'r') as file:
        return json.load(file)

def save_json_records(records, json_path):
    """
    Saves modified JSON records to the same file, overwriting the original content.
    Args:
        records (list): The modified list of dictionaries to write back to JSON.
        json_path (str): Path to the JSON file to save updates.
    """
    with open(json_path, 'w') as file:
        json.dump(records, file, indent=4)  # Pretty printing the output JSON.

def extract_unique_conversion_action_names(records):
    """
    Extracts unique conversion action names from records.
    Args:
        records (list): A list of dictionary records.
    Returns:
        set: A set containing unique conversion action names.
    """
    unique_names = set()
    for record in records:
        if "conversion_action_name" in record:
            unique_names.add(record["conversion_action_name"])
    return unique_names

def fetch_conversion_action_id(client, customer_id, conversion_name):
    """
    Fetches the conversion action ID using the conversion action name set in Google Ads.
    Args:
        client: The Google Ads API client.
        customer_id (str): The Google Ads customer ID.
        conversion_name (str): The name of the conversion action as configured in Google Ads.
    Returns:
        str: The conversion action ID from Google Ads, or None if not found.
    """
    query = f"""
        SELECT
            conversion_action.id
        FROM conversion_action
        WHERE conversion_action.name = '{conversion_name}'
    """
    ga_service = client.get_service("GoogleAdsService")
    response = ga_service.search_stream(customer_id=customer_id, query=query)

    for batch in response:
        for row in batch.results:
            return row.conversion_action.id  # Return the numerical ID directly

    return None

def main(customer_id, json_path):
    client = GoogleAdsClient.load_from_storage()

    records = load_json_records(json_path)
    unique_names = extract_unique_conversion_action_names(records)

    # Fetch conversion_action_ids for each unique conversion_action_name
    conversion_actions_map = {
        name: fetch_conversion_action_id(client, customer_id, name) for name in unique_names
    }

    # Update records with conversion_action_id
    for record in records:
        if "conversion_action_name" in record:
            conversion_name = record["conversion_action_name"]
            if conversion_name in conversion_actions_map:
                record["conversion_action_id"] = conversion_actions_map[conversion_name]

    # Save the updated records back to the JSON file
    save_json_records(records, json_path)

    print("Getting conversion_action_id for future API request is finished.")

if __name__ == "__main__":
    os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

    parser = argparse.ArgumentParser(description="Update JSON records with conversion_action_ids from Google Ads API.")
    parser.add_argument("-f", "--file_path", required=True, type=str, help="Path to the JSON file containing records.")
    parser.add_argument("-c", "--customer_id", required=True, type=str, help="Customer ID for Google Ads API.")
    
    args = parser.parse_args()

    main(args.customer_id, args.file_path)

To run this code, type the following into the terminal. Make sure to replace {path} with the directory path where your hashed and normalized JSON file is located. Additionally, exchange {customer_id} with the ID of the account that handles your conversions.

python3 inserting_conversion_action_id.py --file_path {path} --customer_id {customerID}

By the end of this operation, you should get conversion_action_id inserted into your JSON file.

2. Creates batches of data

Prior to creating ConversionAdjustment objects and dispatching them to the API, we must first divide our observations into batches. This is necessary to maximize the number of ConversionAdjustments per API call and minimize the number of calls made while adhering to Google API’s restriction of no more than 2,000 objects per call.

To accomplish this, we’ll develop a dedicated script. Start by opening the terminal and entering the following command:

touch EC/creating_batches_2000.py
nano EC/creating_batches_2000.py

After opening the terminal, enter this code, save the changes, and exit the nano editor:

import json
import os
import argparse
from datetime import datetime

def chunk_and_save_json(input_path, output_path=None):
    # Use current directory if no output path is provided
    if output_path is None:
        output_path = os.getcwd()

    # Generate timestamped directory name
    date_stamp = datetime.now().strftime("%d_%m_%Y")
    batch_folder_name = f"{date_stamp}_batched"
    batch_folder_path = os.path.join(output_path, batch_folder_name)

    # Create the directory if it does not exist
    if not os.path.exists(batch_folder_path):
        os.makedirs(batch_folder_path)

    # Read the original JSON data
    with open(input_path, 'r') as file:
        data = json.load(file)

    # Determine number of batches
    batch_size = 2000
    num_batches = (len(data) + batch_size - 1) // batch_size # rounds up

    # Create and save each batch
    for i in range(num_batches):
        batch = data[i*batch_size:(i+1)*batch_size]
        batch_filename = f"{i+1}.json"
        batch_file_path = os.path.join(batch_folder_path, batch_filename)
        
        # Write the batch to a new JSON file
        with open(batch_file_path, 'w') as outfile:
            json.dump(batch, outfile, indent=4)

    print(f"Batches successfully saved in {batch_folder_path}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Chunk JSON data into batches and save.")
    parser.add_argument("input_file", type=str, help="Path to the input JSON file.")
    parser.add_argument("-o", "--output_path", type=str, help="Optional output path to save the batched files.")
    
    args = parser.parse_args()
    chunk_and_save_json(args.input_file, args.output_path)

To run the function, input the following command into the terminal. The first argument needs to be the path to the JSON file with conversion records, where sensitive data has been hashed and the ‘conversion_action_id’ field has been added, as per our previous step. The second argument should define the location to store the function’s output.

python3 EC/creating_batches_2000.py {path} -o {path2}

This output will appear in a folder named in the format {DD_MM_YYYY}_batched. Within this folder, you will find several JSON files named sequentially, such as 1.json, 2.json, 3.json, etc.

3. Create ConversionAdjustment objects and send to Google Ads API

Once the data has been normalized, hashed, and grouped into batches, it is ready to be processed and submitted to the Google Ads API. We will employ the script we crafted to handle the files we created earlier, assembling a list of ConversionAdjustment objects which we will then upload to the Google Ads API.

First, we need to create the file:

touch EC/create_conversionAdjustment_send_api.py
nano EC/create_conversionAdjustment_send_api.py

Next, populate the file with the following code. Be sure to replace {path} with the actual path to your google-ads.yaml file.

import os
import json
import argparse
from google.ads.googleads.client import GoogleAdsClient

# Set the configuration file path directly in the script
os.environ["GOOGLE_ADS_CONFIGURATION_FILE_PATH"] = "{path}"

def create_conversion_adjustment_from_json(client, json_object, customer_id):
    # Create and populate a conversion adjustment object
    conversion_adjustment = client.get_type("ConversionAdjustment")
    conversion_adjustment.adjustment_type = client.enums.ConversionAdjustmentTypeEnum.ENHANCEMENT
    conversion_action_service = client.get_service("ConversionActionService")
    conversion_adjustment.conversion_action = conversion_action_service.conversion_action_path(
        customer_id, json_object['conversion_action_id']
    )
    conversion_adjustment.order_id = json_object["order_id"]
    if "email" in json_object:
        email_identifier = client.get_type("UserIdentifier")
        email_identifier.user_identifier_source = client.enums.UserIdentifierSourceEnum.FIRST_PARTY
        email_identifier.hashed_email = json_object["email"]
        conversion_adjustment.user_identifiers.append(email_identifier)
    return conversion_adjustment

def is_partial_failure_error_present(response):
    """Check if there is a partial failure in the response."""
    partial_failure = getattr(response, "partial_failure_error", None)
    return partial_failure is not None and len(partial_failure.details) > 0

def print_results(client, response):
    """Prints whether a partial failure occurred for the batch upload."""
    if is_partial_failure_error_present(response):
        print("Partial failure occurred for this upload. Please check the details below.")
        partial_failure = getattr(response, "partial_failure_error", None)
        error_details = getattr(partial_failure, "details", [])
        for error_detail in error_details:
            failure_message = client.get_type("GoogleAdsFailure")
            GoogleAdsFailure = type(failure_message)
            failure_object = GoogleAdsFailure.deserialize(error_detail.value)
            for error in failure_object.errors:
                    print(
                        "A partial failure at index "
                        f"{error.location.field_path_elements[0].index} occurred "
                        f"\nError message: {error.message}\nError code: "
                        f"{error.error_code}"
                    )
    else:
        print("No partial failures occurred for this upload.")
            


def upload_adjustments_from_folder(folder_path, customer_id):
    """Upload adjustments from a specified folder."""
    googleads_client = GoogleAdsClient.load_from_storage()
    for filename in os.listdir(folder_path):
        if not filename.endswith(".json"):
            continue
        file_path = os.path.join(folder_path, filename)
        with open(file_path, 'r') as file:
            data = json.load(file)
        conversion_adjustments = [create_conversion_adjustment_from_json(googleads_client, item, customer_id) for item in data if item]
        if conversion_adjustments:
            service = googleads_client.get_service("ConversionAdjustmentUploadService")
            response = service.upload_conversion_adjustments(customer_id=customer_id, conversion_adjustments=conversion_adjustments, partial_failure=True)
            print(f"Upload completed for {filename}.")
            print_results(googleads_client, response)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Upload conversion adjustments from batched JSON files.")
    parser.add_argument("folder_path", type=str, help="Path to the folder containing batched JSON files.")
    parser.add_argument("customer_id", type=str, help="The Google Ads customer ID.")
    args = parser.parse_args()
    upload_adjustments_from_folder(args.folder_path, args.customer_id)

Given the complexity involved, here’s a concise breakdown: The script provided automates the uploading of conversion adjustments to Google Ads from JSON files within a designated folder, ensuring effective API interaction management. It starts by establishing the Google Ads API client’s configuration path through an environmental variable. The create_conversion_adjustment_from_json function generates conversion adjustment objects from JSON data, setting parameters such as adjustment type and associated conversion actions, with the option to include user identifiers. To manage API responses, particularly errors, the script uses is_partial_failure_error_present to identify any partial failures by examining response error details. Errors and overall results from batch uploads are logged by the print_results function. The core function, upload_adjustments_from_folder, sequentially processes and uploads batches of conversion adjustments from each JSON file in the specified directory, accommodating partial failures efficiently. The script, executable via command line, allows input for the folder path and customer ID, facilitating scalable and robust operations that are crucial for handling substantial datasets and enhancing error transparency with detailed logging.

To execute this code, enter the following in the terminal. Ensure you replace {path} with the path to the folder containing the batched JSON files. Also, substitute {customer_id} with the ID of the account managing your conversions.

python3 EC/create_conversionAdjustment_send_api.py {path} {customerID}

If executed properly, the terminal should display a message similar to this: “Upload completed for file1.json. No partial failures occurred for this upload.”

Important considerations before using in production

Consider the following considerations to ensure optimal functionality and compliance with Google Ads API requirements:

  1. User Identifier Utilization: Our code example utilizes ‘email’ as the user identifier. The Google Ads API permits up to five identifiers in each ConversionAdjustment object, with more identifiers often enhancing match accuracy. Always verify and adjust your script against the Official Google documentation, placing each identifier into its own object.
  2. Linking Order IDs: Ensure the order_id in your ConversionAdjustment matches the order ID issued by the tag on your website, as covered in Step 4 of Part I. This identifier is critical for Google Ads to locate the conversion and confirm pre-conversion ad interactions.
  3. Setting Adjustment Types: The adjustment_type should be designated as “ENHANCEMENT.”
  4. Data Upload Protocols: Upload to your Google Ads conversion customer with partial_failure enabled to handle any errors gracefully. Remember that all data uploads should be completed within 24 hours of capturing the initial conversion to maintain data integrity.

Verifying Upload Completion

There are numerous potential issues you could encounter when setting up your EC for the first time, and discussing them all would require an additional article. For the moment, I recommend closely examining any errors listed in the ‘Partial Failure Error’ field of the response and addressing them as per the Google Ads API documentation. Furthermore, please make sure to utilize the Enhanced Conversion API diagnostic report and Offline Data Diagnostics to aid in troubleshooting.

Conclusion

This guide on implementing Enhanced Conversions for the Web with the Google Ads API illustrates the intricate and comprehensive nature of the setup process, which entails much more than coding—it involves validating initial settings, confirming conversion actions, accepting customer data terms, and establishing effective web-based conversion tracking. If you find this guide helpful, please like and share it to assist others in mastering Enhanced Conversions through the Google Ads API, optimizing their marketing strategies for more accurate and impactful results.


If you found this article useful and would like to support my work, consider buying me a coffee! Just click on the button below and follow the instructions to make a donation. Your support helps me create more valuable content. Thank you!