From aed3fef2d04e9005815eb500e68c9d31d76f1e98 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Tue, 20 Oct 2026 14:07:43 +0000 Subject: [PATCH 1/3] feat: add sample for creating a customer match user list --- samples/audiences/create_user_list.py | 205 ++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 samples/audiences/create_user_list.py diff --git a/samples/audiences/create_user_list.py b/samples/audiences/create_user_list.py new file mode 100644 index 0000000..873e411 --- /dev/null +++ b/samples/audiences/create_user_list.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Sample of creating a user list in an operating account.""" + +import argparse +import datetime +import logging +from typing import Optional + +from google.ads import datamanager_v1 + +_logger = logging.getLogger(__name__) + + +# [START create-user-list] +def main( + operating_account_type: datamanager_v1.ProductAccount.AccountType, + operating_account_id: str, + validate_only: bool, + login_account_type: Optional[ + datamanager_v1.ProductAccount.AccountType + ] = None, + login_account_id: Optional[str] = None, + linked_account_type: Optional[ + datamanager_v1.ProductAccount.AccountType + ] = None, + linked_account_id: Optional[str] = None, +) -> None: + """Runs the sample. + + Args: + operating_account_type: the account type of the operating account. + operating_account_id: the ID of the operating account. + validate_only: whether to enable validate_only on the request. + login_account_type: the account type of the login account. + login_account_id: the ID of the login account. + linked_account_type: the account type of the linked account. + linked_account_id: the ID of the linked account. + """ + + # Validates parameter pairs. + if bool(login_account_type) != bool(login_account_id): + raise ValueError( + "Must specify either both or neither of login account type and " + "login account ID" + ) + if bool(linked_account_type) != bool(linked_account_id): + raise ValueError( + "Must specify either both or neither of linked account type and " + "linked account ID" + ) + + # Optional: Sets headers to specify the login-account and linked-account if + # your credentials aren't for a user in the operating account (for example, + # when authenticating through a Google Ads manager account or Data Partner + # account). + headers = [] + + if login_account_id: + headers.append( + ( + "login-account", + f"accountTypes/{login_account_type}/accounts/{login_account_id}", + ) + ) + + if linked_account_id: + headers.append( + ( + "linked-account", + f"accountTypes/{linked_account_type}/accounts/{linked_account_id}", + ) + ) + + # Configures the Customer Match user list for contact information. + # For more details on constructing a UserList, see: + # https://developers.google.com/data-manager/api/devguides/audiences/google-ads/customer-match/create-audience + user_list = datamanager_v1.UserList( + # Required. The display name must be unique among all lists owned by the + # account. + display_name=( + "Contact info audience - " + f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ), + description="Customer Match for contact info", + # Optional. The duration an audience member remains in the list after + # being added. Must correspond to whole days (defaults to maximum if + # not set). + membership_duration=datetime.timedelta(days=30), + # Required for a Customer Match audience. + ingested_user_list_info=datamanager_v1.IngestedUserListInfo( + # Sets to a list containing a single UploadKeyType. Use CONTACT_ID + # for hashed email addresses, phone numbers, or physical addresses. + upload_key_types=[ + datamanager_v1.IngestedUserListInfo.UploadKeyType.CONTACT_ID + ], + # Additional metadata required when CONTACT_ID is specified. + contact_id_info=datamanager_v1.ContactIdInfo( + data_source_type=( + datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY + ) + ), + ), + ) + + # Creates a client for UserListService. + user_list_client = datamanager_v1.UserListServiceClient() + + # Builds the request targeting the parent operating account. + request = datamanager_v1.CreateUserListRequest( + parent=f"accountTypes/{operating_account_type}/accounts/{operating_account_id}", + user_list=user_list, + # Sets validate_only. If true, then the Data Manager API only + # validates the request but doesn't apply changes. + validate_only=validate_only, + ) + + # Sends the request with any configured header metadata. + response: datamanager_v1.UserList = user_list_client.create_user_list( + request=request, + metadata=headers, + ) + + # Logs the created user list. When validate_only is false, response.id + # contains the created user list ID, which you pass as the + # product_destination_id in subsequent ingest or remove requests. + _logger.info("Response:\n%s", response) + # [END create-user-list] + + +if __name__ == "__main__": + # Configures logging. + logging.basicConfig(level=logging.INFO) + + parser = argparse.ArgumentParser( + description="Creates a user list in an operating account.", + fromfile_prefix_chars="@", + ) + # The following argument(s) should be provided to run the example. + parser.add_argument( + "--operating_account_type", + type=str, + required=True, + help="The account type of the operating account.", + ) + parser.add_argument( + "--operating_account_id", + type=str, + required=True, + help="The ID of the operating account.", + ) + parser.add_argument( + "--login_account_type", + type=str, + required=False, + help="The account type of the login account.", + ) + parser.add_argument( + "--login_account_id", + type=str, + required=False, + help="The ID of the login account.", + ) + parser.add_argument( + "--linked_account_type", + type=str, + required=False, + help="The account type of the linked account.", + ) + parser.add_argument( + "--linked_account_id", + type=str, + required=False, + help="The ID of the linked account.", + ) + parser.add_argument( + "--validate_only", + choices=["true", "false"], + default="true", + help="Whether to enable validate_only on the request. Must be 'true' " + "or 'false'. Defaults to 'true'.", + ) + args = parser.parse_args() + + main( + args.operating_account_type, + args.operating_account_id, + args.validate_only == "true", + args.login_account_type, + args.login_account_id, + args.linked_account_type, + args.linked_account_id, + ) From bc868c0d654d2cbe813049c624ee3a372cad7c59 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Tue, 20 Oct 2026 15:39:56 +0000 Subject: [PATCH 2/3] feat: add parameters for MOBILE_ID and USER_ID user lists --- samples/audiences/create_user_list.py | 119 ++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/samples/audiences/create_user_list.py b/samples/audiences/create_user_list.py index 873e411..f4466cd 100644 --- a/samples/audiences/create_user_list.py +++ b/samples/audiences/create_user_list.py @@ -29,6 +29,9 @@ def main( operating_account_type: datamanager_v1.ProductAccount.AccountType, operating_account_id: str, validate_only: bool, + upload_key_type: datamanager_v1.IngestedUserListInfo.UploadKeyType = ( + datamanager_v1.IngestedUserListInfo.UploadKeyType.CONTACT_ID + ), login_account_type: Optional[ datamanager_v1.ProductAccount.AccountType ] = None, @@ -37,6 +40,8 @@ def main( datamanager_v1.ProductAccount.AccountType ] = None, linked_account_id: Optional[str] = None, + key_space: Optional[datamanager_v1.MobileIdInfo.KeySpace] = None, + app_id: Optional[str] = None, ) -> None: """Runs the sample. @@ -44,13 +49,18 @@ def main( operating_account_type: the account type of the operating account. operating_account_id: the ID of the operating account. validate_only: whether to enable validate_only on the request. + upload_key_type: the upload key type of the user list. login_account_type: the account type of the login account. login_account_id: the ID of the login account. linked_account_type: the account type of the linked account. linked_account_id: the ID of the linked account. + key_space: the key space of mobile IDs. Required when upload_key_type is + MOBILE_ID. + app_id: the mobile application ID. Required when upload_key_type is + MOBILE_ID. """ - # Validates parameter pairs. + # Validates parameters. if bool(login_account_type) != bool(login_account_id): raise ValueError( "Must specify either both or neither of login account type and " @@ -61,6 +71,20 @@ def main( "Must specify either both or neither of linked account type and " "linked account ID" ) + if ( + upload_key_type + == datamanager_v1.IngestedUserListInfo.UploadKeyType.MOBILE_ID + ): + if not key_space or not app_id: + raise ValueError( + "Must specify both key_space and app_id when upload_key_type " + "is MOBILE_ID" + ) + elif key_space or app_id: + raise ValueError( + "key_space and app_id are only supported when upload_key_type is " + "MOBILE_ID" + ) # Optional: Sets headers to specify the login-account and linked-account if # your credentials aren't for a user in the operating account (for example, @@ -84,35 +108,67 @@ def main( ) ) - # Configures the Customer Match user list for contact information. + # Configures the Customer Match user list. # For more details on constructing a UserList, see: # https://developers.google.com/data-manager/api/devguides/audiences/google-ads/customer-match/create-audience + ingested_user_list_info = datamanager_v1.IngestedUserListInfo( + # Sets to a list containing a single UploadKeyType. + upload_key_types=[upload_key_type], + ) + + if ( + upload_key_type + == datamanager_v1.IngestedUserListInfo.UploadKeyType.CONTACT_ID + ): + # Use CONTACT_ID for hashed email addresses, phone numbers, or physical + # addresses. Additional metadata is required when CONTACT_ID is + # specified. + ingested_user_list_info.contact_id_info = datamanager_v1.ContactIdInfo( + data_source_type=( + datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY + ) + ) + elif ( + upload_key_type + == datamanager_v1.IngestedUserListInfo.UploadKeyType.USER_ID + ): + # Use USER_ID for first-party user IDs assigned to logged-in users. + # Additional metadata is required when USER_ID is specified. + ingested_user_list_info.user_id_info = datamanager_v1.UserIdInfo( + data_source_type=( + datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY + ) + ) + elif ( + upload_key_type + == datamanager_v1.IngestedUserListInfo.UploadKeyType.MOBILE_ID + ): + # Use MOBILE_ID for mobile advertising IDs (IDFA or AAID). Additional + # metadata is required when MOBILE_ID is specified. + ingested_user_list_info.mobile_id_info = datamanager_v1.MobileIdInfo( + data_source_type=( + datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY + ), + key_space=key_space, + app_id=app_id, + ) + else: + raise ValueError(f"Unsupported upload key type: {upload_key_type}") + user_list = datamanager_v1.UserList( # Required. The display name must be unique among all lists owned by the # account. display_name=( - "Contact info audience - " + f"Customer Match ({upload_key_type.name}) - " f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ), - description="Customer Match for contact info", + description=f"Customer Match audience for {upload_key_type.name}", # Optional. The duration an audience member remains in the list after # being added. Must correspond to whole days (defaults to maximum if # not set). membership_duration=datetime.timedelta(days=30), # Required for a Customer Match audience. - ingested_user_list_info=datamanager_v1.IngestedUserListInfo( - # Sets to a list containing a single UploadKeyType. Use CONTACT_ID - # for hashed email addresses, phone numbers, or physical addresses. - upload_key_types=[ - datamanager_v1.IngestedUserListInfo.UploadKeyType.CONTACT_ID - ], - # Additional metadata required when CONTACT_ID is specified. - contact_id_info=datamanager_v1.ContactIdInfo( - data_source_type=( - datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY - ) - ), - ), + ingested_user_list_info=ingested_user_list_info, ) # Creates a client for UserListService. @@ -161,6 +217,12 @@ def main( required=True, help="The ID of the operating account.", ) + parser.add_argument( + "--upload_key_type", + choices=["CONTACT_ID", "USER_ID", "MOBILE_ID"], + default="CONTACT_ID", + help="The upload key type of the user list. Defaults to 'CONTACT_ID'.", + ) parser.add_argument( "--login_account_type", type=str, @@ -185,6 +247,22 @@ def main( required=False, help="The ID of the linked account.", ) + parser.add_argument( + "--key_space", + choices=["ANDROID", "IOS"], + required=False, + help="The key space of mobile IDs ('ANDROID' or 'IOS'). Required when " + "--upload_key_type is 'MOBILE_ID'.", + ) + parser.add_argument( + "--app_id", + type=str, + required=False, + help="A string that uniquely identifies a mobile application from " + "which the data was collected (for example, 'com.labpixies.flood' for " + "Android or '476943146' for iOS). Required when --upload_key_type is " + "'MOBILE_ID'.", + ) parser.add_argument( "--validate_only", choices=["true", "false"], @@ -198,8 +276,15 @@ def main( args.operating_account_type, args.operating_account_id, args.validate_only == "true", + datamanager_v1.IngestedUserListInfo.UploadKeyType[args.upload_key_type], args.login_account_type, args.login_account_id, args.linked_account_type, args.linked_account_id, + ( + datamanager_v1.MobileIdInfo.KeySpace[args.key_space] + if args.key_space + else None + ), + args.app_id, ) From a4d23fc7a9af0b53066cb5c0d43126a0bf5db7ba Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Tue, 20 Oct 2026 15:44:51 +0000 Subject: [PATCH 3/3] fix: update comments --- samples/audiences/create_user_list.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/audiences/create_user_list.py b/samples/audiences/create_user_list.py index f4466cd..5a0b7c3 100644 --- a/samples/audiences/create_user_list.py +++ b/samples/audiences/create_user_list.py @@ -121,8 +121,7 @@ def main( == datamanager_v1.IngestedUserListInfo.UploadKeyType.CONTACT_ID ): # Use CONTACT_ID for hashed email addresses, phone numbers, or physical - # addresses. Additional metadata is required when CONTACT_ID is - # specified. + # addresses. ingested_user_list_info.contact_id_info = datamanager_v1.ContactIdInfo( data_source_type=( datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY @@ -133,7 +132,6 @@ def main( == datamanager_v1.IngestedUserListInfo.UploadKeyType.USER_ID ): # Use USER_ID for first-party user IDs assigned to logged-in users. - # Additional metadata is required when USER_ID is specified. ingested_user_list_info.user_id_info = datamanager_v1.UserIdInfo( data_source_type=( datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY @@ -143,8 +141,7 @@ def main( upload_key_type == datamanager_v1.IngestedUserListInfo.UploadKeyType.MOBILE_ID ): - # Use MOBILE_ID for mobile advertising IDs (IDFA or AAID). Additional - # metadata is required when MOBILE_ID is specified. + # Use MOBILE_ID for mobile advertising IDs. ingested_user_list_info.mobile_id_info = datamanager_v1.MobileIdInfo( data_source_type=( datamanager_v1.DataSourceType.DATA_SOURCE_TYPE_FIRST_PARTY