How to use Google Ads API to upload audiences for customer match

1st party data is the new gold. How do you upload to Google Ads to activate them?

There are many reasons why you should upload your audiences to Google ads.

Some benefits:

  1. Reach the Right People: By uploading your customer data, you can create highly targeted campaigns that reach people who have already shown interest in your products or services.
  2. Tailored Messaging: You can create custom ads and landing pages that resonate with specific audience segments based on their demographics, interests, or past interactions with your brand. This leads to a more personalized experience, boosting engagement and conversions.
  3. Lookalike Audience: Create lookalike audiences based on your existing users. This help you to reach new high value users.
  4. Improve Bidding Signals: Customer Match lists provide valuable first-party data about your existing customers, including their demographics, purchase history, and interests. This information can be used by Smart Bidding algorithms to better understand user behavior and predict the likelihood of conversion.

Once we understand that there are a lot of benefits in uploading your existing Audience lists, we can look at how we could do that.

The easiest way is to go to Google Ads UI and just upload it or schedule it to fetch it from your server via SFTP. BUT what if you want to be fancy and upload via Google Ads API? Let’s go and see how we make that happened.

Screenshot by Author from Google Youtube tutorial

Requirement

Make sure you got this done:

  1. Google Ads API Config File. You can check out this guide of mine if you haven't set it up.
  2. Customer PII List or GCLID List
  3. A development environment for API calls (using a client library or REST). For this guide, we will use Python as the scripting language.

Let’s Go

Set up the environment

  • Open up google-ads-python/google-ads.yaml

Fill up the config file(Google-ads.yaml). If you need help, look at this guide.

  • Import required library
import hashlib
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

Create an Empty Customer Match List

  • Create a function to create an empty Customer Match List in Google Ads
def create_customer_match_user_list(client, customer_id):
    """Creates a Customer Match user list.

    Args:
        client: The Google Ads client.
        customer_id: The ID for the customer that owns the user list.

    Returns:
        The string resource name of the newly created user list.
    """
    # Creates the UserListService client.
    user_list_service_client = client.get_service("UserListService")

    # Creates the user list operation.
    user_list_operation = client.get_type("UserListOperation")

    # Creates the new user list.
    user_list = user_list_operation.create
    user_list.name = f"Customer Match list #{uuid.uuid4()}"
    user_list.description = (
        "A list of customers that originated from email and physical addresses"
    )
    # Sets the upload key type to indicate the type of identifier that is used
    # to add users to the list. This field is immutable and required for a
    # CREATE operation.
    user_list.crm_based_user_list.upload_key_type = (
        client.enums.CustomerMatchUploadKeyTypeEnum.CONTACT_INFO
    )
    # Customer Match user lists can set an unlimited membership life span;
    # to do so, use the special life span value 10000. Otherwise, membership
    # life span must be between 0 and 540 days inclusive. See:
    # https://developers.devsite.corp.google.com/google-ads/api/reference/rpc/latest/UserList#membership_life_span
    # Sets the membership life span to 30 days.
    user_list.membership_life_span = 30

    response = user_list_service_client.mutate_user_lists(
        customer_id=customer_id, operations=[user_list_operation]
    )
    user_list_resource_name = response.results[0].resource_name
    print(
        f"User list with resource name '{user_list_resource_name}' was created."
    )

    return user_list_resource_name

Prepare your Data

Google will require email/phone number/address to be hashed in SHA256. We will focus just on email today.

Create a function to normalize(remove white space & lower case) and hash them in SHA256

def normalize_and_hash(s, remove_all_whitespace):
    """Normalizes and hashes a string with SHA-256.

    Args:
        s: The string to perform this operation on.
        remove_all_whitespace: If true, removes leading, trailing, and
            intermediate spaces from the string before hashing. If false, only
            removes leading and trailing spaces from the string before hashing.

    Returns:
        A normalized (lowercase, remove whitespace) and SHA-256 hashed string.
    """
    # Normalizes by first converting all characters to lowercase, then trimming
    # spaces.
    if remove_all_whitespace:
        # Removes leading, trailing, and intermediate whitespace.
        s = "".join(s.split())
    else:
        # Removes only leading and trailing spaces.
        s = s.strip().lower()

    # Hashes the normalized string using the hashing algorithm.
    return hashlib.sha256(s.encode()).hexdigest()

Upload List to job operation

Build a function to add email list into a job operation

def build_offline_user_data_job_operations(client, emailList):
  operations = []
  for email in emailList:
        # Creates a UserData object that represents a member of the user list.
        user_data = client.get_type("UserData")
        user_identifier = client.get_type("UserIdentifier")
        user_identifier.hashed_email = normalize_and_hash(
            record["email"], True
        )
        # Adds the hashed email identifier to the UserData object's list.
        user_data.user_identifiers.append(user_identifier)

   # If the user_identifiers repeated field is not empty, create a new
   # OfflineUserDataJobOperation and add the UserData to it.
        if user_data.user_identifiers:
            operation = client.get_type("OfflineUserDataJobOperation")
            operation.create = user_data
            operations.append(operation)

return operations;

Add job operation into an upload request

def add_users_to_customer_match_user_list(
    client,
    customer_id,
    user_list_resource_name,
    run_job,
    offline_user_data_job_id,
    ad_user_data_consent,
    ad_personalization_consent,
    emailList
):
    """Uses Customer Match to create and add users to a new user list.

    Args:
        client: The Google Ads client.
        customer_id: The ID for the customer that owns the user list.
        user_list_resource_name: The resource name of the user list to which to
            add users.
        run_job: If true, runs the OfflineUserDataJob after adding operations.
            Otherwise, only adds operations to the job.
        offline_user_data_job_id: ID of an existing OfflineUserDataJob in the
            PENDING state. If None, a new job is created.
        ad_user_data_consent: The consent status for ad user data for all
            members in the job.
        ad_personalization_consent: The personalization consent status for ad
            user data for all members in the job.
    """
    # Creates the OfflineUserDataJobService client.
    offline_user_data_job_service_client = client.get_service(
        "OfflineUserDataJobService"
    )

    if offline_user_data_job_id:
        # Reuses the specified offline user data job.
        offline_user_data_job_resource_name = (
            offline_user_data_job_service_client.offline_user_data_job_path(
                customer_id, offline_user_data_job_id
            )
        )
    else:
        # Creates a new offline user data job.
        offline_user_data_job = client.get_type("OfflineUserDataJob")
        offline_user_data_job.type_ = (
            client.enums.OfflineUserDataJobTypeEnum.CUSTOMER_MATCH_USER_LIST
        )
        offline_user_data_job.customer_match_user_list_metadata.user_list = (
            user_list_resource_name
        )

        # Specifies whether user consent was obtained for the data you are
        # uploading. For more details, see:
        # https://www.google.com/about/company/user-consent-policy
        if ad_user_data_consent:
            offline_user_data_job.customer_match_user_list_metadata.consent.ad_user_data = client.enums.ConsentStatusEnum[
                ad_user_data_consent
            ]
        if ad_personalization_consent:
            offline_user_data_job.customer_match_user_list_metadata.consent.ad_personalization = client.enums.ConsentStatusEnum[
                ad_personalization_consent
            ]

        # Issues a request to create an offline user data job.
        create_offline_user_data_job_response = (
            offline_user_data_job_service_client.create_offline_user_data_job(
                customer_id=customer_id, job=offline_user_data_job
            )
        )
        offline_user_data_job_resource_name = (
            create_offline_user_data_job_response.resource_name
        )
        print(
            "Created an offline user data job with resource name: "
            f"'{offline_user_data_job_resource_name}'."
        )

    # Issues a request to add the operations to the offline user data job.

    # Best Practice: This example only adds a few operations, so it only sends
    # one AddOfflineUserDataJobOperations request. If your application is adding
    # a large number of operations, split the operations into batches and send
    # multiple AddOfflineUserDataJobOperations requests for the SAME job. See
    # https://developers.google.com/google-ads/api/docs/remarketing/audience-types/customer-match#customer_match_considerations
    # and https://developers.google.com/google-ads/api/docs/best-practices/quotas#user_data
    # for more information on the per-request limits.
    request = client.get_type("AddOfflineUserDataJobOperationsRequest")
    request.resource_name = offline_user_data_job_resource_name
    request.operations = build_offline_user_data_job_operations(client, emailList)
    request.enable_partial_failure = True

    # Issues a request to add the operations to the offline user data job.
    response = offline_user_data_job_service_client.add_offline_user_data_job_operations(
        request=request
    )

    # Prints the status message if any partial failure error is returned.
    # Note: the details of each partial failure error are not printed here.
    # Refer to the error_handling/handle_partial_failure.py example to learn
    # more.
    # Extracts the partial failure from the response status.
    partial_failure = getattr(response, "partial_failure_error", None)
    if getattr(partial_failure, "code", None) != 0:
        error_details = getattr(partial_failure, "details", [])
        for error_detail in error_details:
            failure_message = client.get_type("GoogleAdsFailure")
            # Retrieve the class definition of the GoogleAdsFailure instance
            # in order to use the "deserialize" class method to parse the
            # error_detail string into a protobuf message object.
            failure_object = type(failure_message).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.\n"
                    f"Error message: {error.message}\n"
                    f"Error code: {error.error_code}"
                )

    print("The operations are added to the offline user data job.")

    if not run_job:
        print(
            "Not running offline user data job "
            f"'{offline_user_data_job_resource_name}', as requested."
        )
        return

    # Issues a request to run the offline user data job for executing all
    # added operations.
    offline_user_data_job_service_client.run_offline_user_data_job(
        resource_name=offline_user_data_job_resource_name
    )

Last step: Tie it all in

Setup a main function to run your script.

# GoogleAdsClient will read the google-ads.yaml configuration file in the
# home directory if none is specified.
googleads_client = GoogleAdsClient.load_from_storage(version="v16")

if __name__ == "__main__":
  try:
        main(
            googleads_client,
            customer_id,
            run_job,
            user_list_id,
            offline_user_data_job_id,
            ad_user_data_consent,
            ad_personalization_consent,
            emailList
        )
    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)

def main(
    client,
    customer_id,
    run_job,
    user_list_id,
    offline_user_data_job_id,
    ad_user_data_consent,
    ad_personalization_consent,
    emailList
):
    """Uses Customer Match to create and add users to a new user list.

    Args:
        client: The Google Ads client.
        customer_id: The ID for the customer that owns the user list.
        run_job: if True, runs the OfflineUserDataJob after adding operations.
            Otherwise, only adds operations to the job.
        user_list_id: ID of an existing user list. If None, a new user list is
            created.
        offline_user_data_job_id: ID of an existing OfflineUserDataJob in the
            PENDING state. If None, a new job is created.
        ad_user_data_consent: The consent status for ad user data for all
            members in the job.
        ad_personalization_consent: The personalization consent status for ad
            user data for all members in the job.
    """
    googleads_service = client.get_service("GoogleAdsService")

    if not offline_user_data_job_id:
        if user_list_id:
            # Uses the specified Customer Match user list.
            user_list_resource_name = googleads_service.user_list_path(
                customer_id, user_list_id
            )
        else:
            # Creates a Customer Match user list.
            user_list_resource_name = create_customer_match_user_list(
                client, customer_id
            )

    add_users_to_customer_match_user_list(
        client,
        customer_id,
        user_list_resource_name,
        run_job,
        offline_user_data_job_id,
        ad_user_data_consent,
        ad_personalization_consent,
        emailList
    )

All good? Connect with me at https://www.linkedin.com/in/joseph-sian-gou-wei/

A shameless plug here. I am an advertising professional with a mission to teach others about marketing technology. I’m putting Martech tutorials without a paywall.
If you enjoyed this article, please do consider to clap/follow me, or buy me a coffee here!
Cheers friends!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top