Migrating to UserDataService

This guide explains how to migrate from building user lists with AdwordsUserListService in the AdWords API to using UserDataService in the Google Ads API in order to sync Customer Match data.

The Remarketing and Audience Targeting guide also provides useful context about user lists and audience management in the Google Ads API. However, it is not required for understanding the concepts in this guide.

Interface mapping

The following table provides a mapping of the equivalent components and corresponding reference documentation for the AdWords API and Google Ads API.

Note that there are two services available in Google Ads API: UserDataService and OfflineUserDataJobService. Both can upload data for user lists, but OfflineUserDataJobs are processed asynchronously. This document steps through the usage of UserDataService. Complete example code utilizing the OfflineUserDataService is included at the end of the page.

AdWords API ServiceEquivalent Google Ads API Service
UserList management
Service AdWordsUserListService UserListService
Customer Match UserList CrmBasedUserList UserList.crm_based_user_list
Service-specific errors UserListError UserListErrorEnum
UserList member uploads
Service AdWordsUserListService UserDataService

OfflineUserDataJobService

Request MutateMembersOperation UploadUserDataRequest

CreateOfflineUserDataJobRequest and AddOfflineUserDataJobOperationsRequest

Response MutateMembersReturnValue UploadUserDataResponse

CreateOfflineUserDataJobResponse and AddOfflineUserDataJobOperationsResponse

Service-specific errors MutateMembersError UserDataError

OfflineUserDataJobError

Rate limits

The UserDataService has a limit of 10 operations and 100 user IDs per request.

Creating and populating a Customer Match UserList

We now demonstrate how to create and populate a Customer Match user list using UserDataService.

Creating a UserList instance

First, create a Customer Match user list with the UserListService. Set the crm_based_user_list field with a CrmBasedUserListInfo object.

CrmBasedUserListInfo contains three fields:

  • app_id: A string that uniquely identifies the mobile application from which the data was collected. This is required when creating CrmBasedUserList for uploading mobile advertising IDs.
  • upload_key_type: A matching key type of the list, which can be CONTACT_INFO, CRM_ID, or MOBILE_ADVERTISING_ID. Mixed data types are not allowed in the same list. This field is required for all Customer Match userlists.
  • data_source_type: The data source of the list. The default value is FIRST_PARTY. Allowlisted customers may create third-party sourced Customer Match lists.

The membership_life_span attribute of the user list allows you to define the time period, in days, for which a user is considered to be in the list. The membership_status attribute defines whether the list accepts new users. Customer Match user list types allow you to set membership_life_span to 10000 to indicate no expiration.

private void runExample(GoogleAdsClient googleAdsClient, long customerId) {
 // Creates a user list.
 UserList userList =
     UserList.newBuilder()
         .setName(
             StringValue.of(
                 "Contact Info Customer Match UserList example #" + System.currentTimeMillis()))
         .setDescription(StringValue.of("Customer Match UserList containing contact info."))
         .setMembershipStatus(UserListMembershipStatus.OPEN)
         .setMembershipLifeSpan(Int64Value.of(365))
         .setCrmBasedUserList(
             CrmBasedUserListInfo.newBuilder()
                 // Sets the upload key type to CONTACT_INFO to upload an address, hashed email or
                 // hashed phone number.
                 .setUploadKeyType(CustomerMatchUploadKeyType.CONTACT_INFO)
                 .build())
         .build();

 // Creates the operation.
 UserListOperation operation = UserListOperation.newBuilder().setCreate(userList).build();

 // Creates the user list service client.
 try (UserListServiceClient userListServiceClient =
     googleAdsClient.getLatestVersion().createUserListServiceClient()) {
   // Adds the user list.
   MutateUserListsResponse response =
       userListServiceClient.mutateUserLists(
           Long.toString(customerId), ImmutableList.of(operation));
   String userListResourceName = response.getResults(0).getResourceName();
   // Prints the result.
   System.out.printf("Created user list with resource name '%s'.%n", userListResourceName);
 }
}

Creating an UploadUserDataRequest instance

Once you have created your Customer Match user list, you can populate it with user data using the UserDataService. The UserDataService contains the UploadUserData method, which accepts an UploadUserDataRequest. In addition to the customer_id, the UploadUserDataRequest accepts a list of the operations to create the contacts and a required field called customer_match_user_list_metadata, which is populated with the resource name of the remarketing list to target.

Begin by creating an UploadUserDataRequest instance in which you will populate the customer_id and customer_match_user_list_metadata:

// Creates a request to add user data operations to the user list based on email addresses.
String userListResourceName = ResourceNames.userList(customerId, userListId);
UploadUserDataRequest.Builder uploadUserDataRequest =
   UploadUserDataRequest.newBuilder()
       .setCustomerId(String.valueOf(customerId))
       .setCustomerMatchUserListMetadata(
           CustomerMatchUserListMetadata.newBuilder()
               .setUserList(StringValue.of(userListResourceName))
               .build());

Adding user contact information

You must now add user data to your user list. Keep in mind that each user list can only contain a single type of user data.

Adding customer contact information

To upload user contact information, set CrmBasedUserListInfo.upload_key_type to CONTACT_INFO.

First, add the operations to the UploadUserDataRequest object. Each operation contains a create field populated with UserData objects that hold one or more UserIdentifier instances. Each UserIdentifier contains one piece of identifying information or can be one of several different types, each of which is described below.

To upload customer email addresses, create a new UserDataOperation and populate its create field with a UserData object. The UserData object accepts a list of user_identifiers. Populate the hashed_email field with the customer email addresses.

ImmutableList<String> EMAILS =
  ImmutableList.of("client1@example.com", "client2@example.com", " Client3@example.com ");

// Hash normalized email addresses based on SHA-256 hashing algorithm.
List<UserDataOperation> userDataOperations = new ArrayList<>(EMAILS.size());
for (String email : EMAILS) {
 UserDataOperation userDataOperation =
     UserDataOperation.newBuilder()
         .setCreate(
             UserData.newBuilder()
                 .addUserIdentifiers(
                     UserIdentifier.newBuilder()
                        .setHashedEmail(StringValue.of(toSHA256String(email)))
                         .build())
                 .build())
         .build();
 userDataOperations.add(userDataOperation);
}
uploadUserDataRequest.addAllOperations(userDataOperations);

Uploading user address_info is similar to uploading user email addresses. However, instead of passing a hashed_email, populate the address_info field with an OfflineUserAddressInfo object containing the user’s first_name, last_name, country_code, and postal_code. Like email addresses, first_name and last_name are considered personally identifiable information and must be hashed before uploading.

String firstName = "John";
String lastName = "Doe";
String countryCode = "US";
String postalCode = "10011";

UserIdentifier userIdentifierWithAddress =
   UserIdentifier.newBuilder()
       .setAddressInfo(
           OfflineUserAddressInfo.newBuilder()
               // First and last name must be normalized and hashed.
               .setHashedFirstName(
                   StringValue.of(toSHA256String(firstName)))
               .setHashedLastName(StringValue.of(toSHA256String(lastName)))
               // Country code and zip code are sent in plaintext.
               .setCountryCode(StringValue.of(countryCode))
               .setPostalCode(StringValue.of(postalCode))
               .build())
       .build();

UserDataOperation userDataOperation =
   UserDataOperation.newBuilder()
       .setCreate(
           UserData.newBuilder()
               .addUserIdentifiers(userIdentifierWithAddress)
               .build())
       .build();
uploadUserDataRequest.addOperations(userDataOperation);

Adding mobile IDs

To upload user information matched from mobile advertising IDs, set CrmBasedUserListInfo.upload_key_type to MOBILE_ADVERTISING_ID.

Uploading a mobile ID to a UserList is done in the same way as for contact information. However, you must populate the mobile_id field of the UserIdentifier object instead of populating the user’s contact information.

ImmutableList<String> MOBILE_IDS =
    ImmutableList.of("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        " YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY ");

// Creates operations with mobile Ids.
List<UserDataOperation> userDataOperations = new ArrayList<>(MOBILE_IDS.size());
for (String mobileId : MOBILE_IDS) {
 UserDataOperation userDataOperation =
     UserDataOperation.newBuilder()
         .setCreate(
             UserData.newBuilder()
                 .addUserIdentifiers(
                     UserIdentifier.newBuilder()
                         .setMobileId(StringValue.of(toNormalizedString(mobileId)))
                         .build())
                 .build())
         .build();
 userDataOperations.add(userDataOperation);
}
uploadUserDataRequest.addAllOperations(userDataOperations);

Adding CRM IDs

To populate the Customer Match user list with CRM IDs, set CrmBasedUserListInfo.upload_key_type to CRM_ID. CRM IDs are matched from a user ID generated and assigned by the advertiser. This is similar to uploading MOBILE_ADVERTISING_ID instances, but you will instead populate the third_party_user_id field of the UserIdentifier object.

ImmutableList<String> CRM_IDS = ImmutableList.of("exampleCrmId1", " exampleCrmId2 ");
// Creates operations with CRM Ids.
List<UserDataOperation> userDataOperations = new ArrayList<>(CRM_IDS.size());
for (String crmId : CRM_IDS) {
 UserDataOperation userDataOperation =
     UserDataOperation.newBuilder()
         .setCreate(
             UserData.newBuilder()
                 .addUserIdentifiers(
                     UserIdentifier.newBuilder()
                         .setThirdPartyUserId(StringValue.of(toNormalizedString(crmId)))
                         .build())
                 .build())
         .build();
 userDataOperations.add(userDataOperation);
}
uploadUserDataRequest.addAllOperations(userDataOperations);

Encoding user data

As outlined in the About the customer matching process support article, private customer data must be hashed before uploading it to a user list. Values should be normalized before hashing, i.e. trimmed of whitespace and converted to lowercase. Normalized data values should then be hashed using the SHA256 algorithm.

/**
* Hashes a string using SHA-256 hashing algorithm.
*
* @param str the string to hash.
* @return the SHA-256 hash string representation.
* @throws UnsupportedEncodingException If UTF-8 charset is not supported.
*/
private static String toSHA256String(String str) throws UnsupportedEncodingException {
 MessageDigest digest = getSHA256MessageDigest();
 byte[] hash = digest.digest(toNormalizedString(str).getBytes("UTF-8"));
 StringBuilder result = new StringBuilder();
 for (byte b : hash) {
   result.append(String.format("%02x", b));
 }

 return result.toString();
}

/** Returns SHA-256 hashing algorithm. */
private static MessageDigest getSHA256MessageDigest() {
 try {
   return MessageDigest.getInstance("SHA-256");
 } catch (NoSuchAlgorithmException e) {
   throw new RuntimeException("Missing SHA-256 algorithm implementation.", e);
 }
}

/**
* Removes leading and trailing whitespace and converts all characters to lowercase.
*
* @param value the string to normalize.
* @return a normalized copy of the string.
*/
private static String toNormalizedString(String value) {
 return value.trim().toLowerCase();
}

Sending the request

After adding operations to the UploadUserDataRequest instance, call the uploadUserData method on the UserDataServiceClient to send the request to the Google Ads API server. You can see that the request was successful by getting the operations count and upload time in the response object. Keep in mind that it may take several hours for the list to be populated with members.

// Creates the user data service client.
try (UserDataServiceClient userDataServiceClient =
   googleAdsClient.getLatestVersion().createUserDataServiceClient()) {
 // Add operations to the user list based on the user data type.
 UploadUserDataResponse response =
     userDataServiceClient.uploadUserData(uploadUserDataRequest.build());

 // Displays the results.
 // Reminder: it may take several hours for the list to be populated with members.
 System.out.printf(
     "Received %d operations at %s",
     response.getReceivedOperationsCount().getValue(),
     response.getUploadDateTime().getValue());
}

Creating a Customer Match UserList using OfflineDataJobService

The example below demonstrates creating a new Customer Match user list and populating it using the OfflineUserDataJobService. Note that this service behaves similarly to UserDataService; see the text above for example code that utilizes UserDataService.

Java

private void addUsersToCustomerMatchUserList(
    GoogleAdsClient googleAdsClient, long customerId, String userListResourceName)
    throws InterruptedException, ExecutionException, TimeoutException,
        UnsupportedEncodingException {
  try (OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
      googleAdsClient.getLatestVersion().createOfflineUserDataJobServiceClient()) {
    // Creates a new offline user data job.
    OfflineUserDataJob offlineUserDataJob =
        OfflineUserDataJob.newBuilder()
            .setType(OfflineUserDataJobType.CUSTOMER_MATCH_USER_LIST)
            .setCustomerMatchUserListMetadata(
                CustomerMatchUserListMetadata.newBuilder()
                    .setUserList(StringValue.of(userListResourceName)))
            .build();

    // Issues a request to create the offline user data job.
    CreateOfflineUserDataJobResponse createOfflineUserDataJobResponse =
        offlineUserDataJobServiceClient.createOfflineUserDataJob(
            Long.toString(customerId), offlineUserDataJob);
    String offlineUserDataJobResourceName = createOfflineUserDataJobResponse.getResourceName();
    System.out.printf(
        "Created an offline user data job with resource name: %s.%n",
        offlineUserDataJobResourceName);

    // Issues a request to add the operations to the offline user data job.
    List<OfflineUserDataJobOperation> userDataJobOperations = buildOfflineUserDataJobOperations();
    AddOfflineUserDataJobOperationsResponse response =
        offlineUserDataJobServiceClient.addOfflineUserDataJobOperations(
            AddOfflineUserDataJobOperationsRequest.newBuilder()
                .setResourceName(offlineUserDataJobResourceName)
                .setEnablePartialFailure(BoolValue.of(true))
                .addAllOperations(userDataJobOperations)
                .build());

    // Prints the status message if any partial failure error is returned.
    // NOTE: The details of each partial failure error are not printed here, you can refer to
    // the example HandlePartialFailure.java to learn more.
    if (response.hasPartialFailureError()) {
      System.out.printf(
          "Encountered %d partial failure errors while adding %d operations to the offline user "
              + "data job: '%s'. Only the successfully added operations will be executed when "
              + "the job runs.%n",
          response.getPartialFailureError().getDetailsCount(),
          userDataJobOperations.size(),
          response.getPartialFailureError().getMessage());
    } else {
      System.out.printf(
          "Successfully added %d operations to the offline user data job.%n",
          userDataJobOperations.size());
    }

    // Issues an asynchronous request to run the offline user data job for executing all added
    // operations.
    OperationFuture<Empty, Empty> runFuture =
        offlineUserDataJobServiceClient.runOfflineUserDataJobAsync(
            offlineUserDataJobResourceName);
    System.out.println("Asynchronous request to execute the added operations started.");
    System.out.println("Waiting until operation completes.");

    // The polling future implements a default back-off policy for retrying.
    runFuture.getPollingFuture().get(MAX_TOTAL_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
    System.out.printf(
        "Offline user data job with resource name '%s' has finished.%n",
        offlineUserDataJobResourceName);
  }
}

C#

private static void AddUsersToCustomerMatchUserList(GoogleAdsClient client,
    long customerId, string userListResourceName)
{
    // Get the OfflineUserDataJobService.
    OfflineUserDataJobServiceClient service = client.GetService(
        Services.V5.OfflineUserDataJobService);

    // Creates a new offline user data job.
    OfflineUserDataJob offlineUserDataJob = new OfflineUserDataJob()
    {
        Type = OfflineUserDataJobType.CustomerMatchUserList,
        CustomerMatchUserListMetadata = new CustomerMatchUserListMetadata()
        {
            UserList = userListResourceName
        }
    };

    // Issues a request to create the offline user data job.
    CreateOfflineUserDataJobResponse response1 = service.CreateOfflineUserDataJob(
        customerId.ToString(), offlineUserDataJob);
    string offlineUserDataJobResourceName = response1.ResourceName;
    Console.WriteLine($"Created an offline user data job with resource name: " +
        $"'{offlineUserDataJobResourceName}'.");

    AddOfflineUserDataJobOperationsRequest request =
        new AddOfflineUserDataJobOperationsRequest()
        {
            ResourceName = offlineUserDataJobResourceName,
            Operations = { BuildOfflineUserDataJobOperations() },
            EnablePartialFailure = true,
        };
    // Issues a request to add the operations to the offline user data job.
    AddOfflineUserDataJobOperationsResponse response2 =
        service.AddOfflineUserDataJobOperations(request);

    // Prints the status message if any partial failure error is returned.
    // Note: The details of each partial failure error are not printed here,
    // you can refer to the example HandlePartialFailure.cs to learn more.
    if (response2.PartialFailureError != null)
    {
        // Extracts the partial failure from the response status.
        GoogleAdsFailure partialFailure = response2.PartialFailure;
        Console.WriteLine($"{partialFailure.Errors.Count} partial failure error(s) " +
            $"occurred");
    }
    Console.WriteLine("The operations are added to the offline user data job.");

    // Issues an asynchronous request to run the offline user data job for executing
    // all added operations.
    Operation<Empty, Empty> operationResponse =
        service.RunOfflineUserDataJob(offlineUserDataJobResourceName);

    Console.WriteLine("Asynchronous request to execute the added operations started."); ;
    Console.WriteLine("Waiting until operation completes.");

    // PollUntilCompleted() implements a default back-off policy for retrying. You can
    // tweak the polling behaviour using a PollSettings as illustrated below.
    operationResponse.PollUntilCompleted(new PollSettings(
        Expiration.FromTimeout(TimeSpan.FromSeconds(MAX_TOTAL_POLL_INTERVAL_SECONDS)),
        TimeSpan.FromSeconds(POLL_FREQUENCY_SECONDS)));

    if (operationResponse.IsCompleted)
    {
        Console.WriteLine($"Offline user data job with resource name " +
            $"'{offlineUserDataJobResourceName}' has finished.");
    }
    else
    {
        Console.WriteLine($"Offline user data job with resource name" +
            $" '{offlineUserDataJobResourceName}' is pending after " +
            $"{MAX_TOTAL_POLL_INTERVAL_SECONDS} seconds.");
    }
}


PHP

private static function addUsersToCustomerMatchUserList(
    GoogleAdsClient $googleAdsClient,
    int $customerId,
    string $userListResourceName
) {
    $offlineUserDataJobServiceClient = $googleAdsClient->getOfflineUserDataJobServiceClient();

    // Creates a new offline user data job.
    $offlineUserDataJob = new OfflineUserDataJob([
        'type' => OfflineUserDataJobType::CUSTOMER_MATCH_USER_LIST,
        'customer_match_user_list_metadata' => new CustomerMatchUserListMetadata([
            'user_list' => new StringValue(['value' => $userListResourceName])
        ])
    ]);

    // Issues a request to create the offline user data job.
    /** @var CreateOfflineUserDataJobResponse $createOfflineUserDataJobResponse */
    $createOfflineUserDataJobResponse =
        $offlineUserDataJobServiceClient->createOfflineUserDataJob(
            $customerId,
            $offlineUserDataJob
        );
    $offlineUserDataJobResourceName = $createOfflineUserDataJobResponse->getResourceName();
    printf(
        "Created an offline user data job with resource name: '%s'.%s",
        $offlineUserDataJobResourceName,
        PHP_EOL
    );

    // Issues a request to add the operations to the offline user data job.
    /** @var AddOfflineUserDataJobOperationsResponse $operationResponse */
    $response = $offlineUserDataJobServiceClient->addOfflineUserDataJobOperations(
        $offlineUserDataJobResourceName,
        self::buildOfflineUserDataJobOperations(),
        ['enablePartialFailure' => new BoolValue(['value' => true])]
    );

    // Prints the status message if any partial failure error is returned.
    // Note: The details of each partial failure error are not printed here, you can refer to
    // the example HandlePartialFailure.php to learn more.
    if (!is_null($response->getPartialFailureError())) {
        // Extracts the partial failure from the response status.
        $partialFailure = GoogleAdsFailures::fromAny(
            $response->getPartialFailureError()->getDetails()->getIterator()->current()
        );
        printf(
            "%d partial failure error(s) occurred: %s.%s",
            count($partialFailure->getErrors()),
            $response->getPartialFailureError()->getMessage(),
            PHP_EOL
        );
    }
    print 'The operations are added to the offline user data job.' . PHP_EOL;

    // Issues an asynchronous request to run the offline user data job for executing all added
    // operations.
    /** @var OperationResponse $operationResponse */
    $operationResponse = $offlineUserDataJobServiceClient->runOfflineUserDataJob(
        $offlineUserDataJobResourceName
    );
    print 'Asynchronous request to execute the added operations started.' . PHP_EOL;
    print 'Waiting until operation completes.' . PHP_EOL;

    // pollUntilComplete() implements a default back-off policy for retrying. You can tweak the
    // retrying parameters like the maximum polling interval to use by passing them as an array
    // to the pollUntilComplete() function. Visit the OperationResponse.php file for more
    // details.
    $operationCompleted = $operationResponse->pollUntilComplete([
        'initialPollDelayMillis' => self::POLL_FREQUENCY_SECONDS * 1000,
        'totalPollTimeoutMillis' => self::MAX_TOTAL_POLL_INTERVAL_SECONDS * 1000
    ]);
    if ($operationCompleted) {
        printf(
            "Offline user data job with resource name '%s' has finished.%s",
            $offlineUserDataJobResourceName,
            PHP_EOL
        );
    } else {
        printf(
            "Offline user data job with resource name '%s' still pending after %d " .
            "seconds, continuing the execution of the code example anyway.%s",
            $offlineUserDataJobResourceName,
            self::MAX_TOTAL_POLL_INTERVAL_SECONDS,
            PHP_EOL
        );
    }
}


Python

def _add_users_to_customer_match_user_list(
    client, customer_id, user_list_resource_name
):
    """Uses Customer Match to create and add users to a new user list.

    Args:
        client: The Google Ads client.
        customer_id: The customer ID for which to add the user list.
        user_list_resource_name: The resource name of the user list to which to
            add users.
    """
    # Creates the OfflineUserDataJobService client.
    offline_user_data_job_service_client = client.get_service(
        "OfflineUserDataJobService", version="v5"
    )

    # Creates a new offline user data job.
    offline_user_data_job = client.get_type("OfflineUserDataJob", version="v5")
    offline_user_data_job.type = client.get_type(
        "OfflineUserDataJobTypeEnum", version="v5"
    ).CUSTOMER_MATCH_USER_LIST
    offline_user_data_job.customer_match_user_list_metadata.user_list.value = (
        user_list_resource_name
    )

    # 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, 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}'."
    )

    true_value = client.get_type("BoolValue", version="v5")
    true_value.value = 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(
            resource_name=offline_user_data_job_resource_name,
            operations=_build_offline_user_data_job_operations(client),
            enable_partial_failure=true_value,
        )
    )

    # 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", version="v5")
            failure_object = failure_message.FromString(error_detail.value)

            for error in failure_object.errors:
                print(
                    "A partial failure at index {} occurred.\n"
                    "Error message: {}\nError code: {}".format(
                        error.location.field_path_elements[0].index.value,
                        error.message,
                        error.error_code,
                    )
                )

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

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

    # Wait until the operation has finished.
    print("Request to execute the added operations started.")
    print("Waiting until operation completes...")
    operation_response.result()

    print(
        "Offline user data job with resource name "
        f"'{offline_user_data_job_resource_name}' has finished."
    )

Ruby

def add_users_to_customer_match_user_list(client, customer_id, user_list)
  # Creates the offline user data job.
  offline_user_data_job = client.resource.offline_user_data_job do |job|
    job.type = :CUSTOMER_MATCH_USER_LIST
    job.customer_match_user_list_metadata =
      client.resource.customer_match_user_list_metadata do |m|
        m.user_list = user_list
      end
  end

  offline_user_data_service = client.service.offline_user_data_job

  # Issues a request to create the offline user data job.
  response = offline_user_data_service.create_offline_user_data_job(
    customer_id: customer_id,
    job: offline_user_data_job,
  )
  offline_user_data_job_resource_name = response.resource_name
  puts "Created an offline user data job with resource name: " \
    "#{offline_user_data_job_resource_name}"

  # Issues a request to add the operations to the offline user data job.
  response = offline_user_data_service.add_offline_user_data_job_operations(
    resource_name: offline_user_data_job_resource_name,
    enable_partial_failure: true,
    operations: build_offline_user_data_job_operations(client),
  )

  # Prints errors if any partial failure error is returned.
  if response.partial_failure_error
    failures = client.decode_partial_failure_error(response.partial_failure_error)
    failures.each do |failure|
      failure.errors.each do |error|
        human_readable_error_path = error
          .location
          .field_path_elements
          .map { |location_info|
            if location_info.index
              "#{location_info.field_name}[#{location_info.index}]"
            else
              "#{location_info.field_name}"
            end
          }.join(" > ")

        errmsg =  "error occured while adding operations " \
          "#{human_readable_error_path}" \
          " with value: #{error.trigger.string_value}" \
          " because #{error.message.downcase}"
        puts errmsg
      end
    end
  end
  puts "The operations are added to the offline user data job."

  # Issues an asynchronous request to run the offline user data job
  # for executing all added operations.
  response = offline_user_data_service.run_offline_user_data_job(
    resource_name: offline_user_data_job_resource_name
  )
  puts "Asynchronous request to execute the added operations started."
  puts "Waiting until operation completes."

  # The wait_until_done! method implements a default backoff policy for
  # retrying.
  # You can also use operation.refresh! to make a call to the API to check
  # whether the LRO is finished, and operation.done? after refreshing to check
  # the status, if you'd rather implement your own backoff logic.
  response.wait_until_done! do |op|
    raise op.results.message if response.error?
  end

  puts "Offline user data job with resource name " \
    "#{offline_user_data_job_resource_name} has finished"
end

Perl

sub add_users_to_customer_match_user_list {
  my ($api_client, $customer_id, $user_list_resource_name) = @_;

  my $offline_user_data_job_service = $api_client->OfflineUserDataJobService();

  # Create a new offline user data job.
  my $offline_user_data_job =
    Google::Ads::GoogleAds::V5::Resources::OfflineUserDataJob->new({
      type => CUSTOMER_MATCH_USER_LIST,
      customerMatchUserListMetadata =>
        Google::Ads::GoogleAds::V5::Common::CustomerMatchUserListMetadata->new({
          userList => $user_list_resource_name
        })});

  # Issue a request to create the offline user data job.
  my $create_offline_user_data_job_response =
    $offline_user_data_job_service->create({
      customerId => $customer_id,
      job        => $offline_user_data_job
    });
  my $offline_user_data_job_resource_name =
    $create_offline_user_data_job_response->{resourceName};
  printf
    "Created an offline user data job with resource name: '%s'.\n",
    $offline_user_data_job_resource_name;

  # Issue a request to add the operations to the offline user data job.
  my $response = $offline_user_data_job_service->add_operations({
      resourceName         => $offline_user_data_job_resource_name,
      enablePartialFailure => "true",
      operations           => build_offline_user_data_job_operations()});

  # Print the status message if any partial failure error is returned.
  # Note: The details of each partial failure error are not printed here, you can
  # refer to the example handle_partial_failure.pl to learn more.
  if ($response->{partialFailureError}) {
    # Extract the partial failure from the response status.
    my $partial_failure = $response->{partialFailureError}{details}[0];
    printf
      "%d partial failure error(s) occurred: %s.\n",
      scalar @{$partial_failure->{errors}},
      $response->{partialFailureError}{message};
  }
  print "The operations are added to the offline user data job.\n";

  # Issue an asynchronous request to run the offline user data job for executing
  # all added operations.
  my $operation_response = $offline_user_data_job_service->run({
    resourceName => $offline_user_data_job_resource_name
  });
  print "Asynchronous request to execute the added operations started.\n";
  print "Waiting until operation completes.\n";

  # poll_until_done() implements a default back-off policy for retrying. You can
  # tweak the parameters like the poll timeout seconds by passing them to the
  # poll_until_done() method. Visit the OperationService.pm file for more details.
  my $lro = $api_client->OperationService()->poll_until_done({
    name                 => $operation_response->{name},
    pollFrequencySeconds => POLL_FREQUENCY_SECONDS,
    pollTimeoutSeconds   => POLL_TIMEOUT_SECONDS
  });
  if ($lro->{done}) {
    printf "Offline user data job with resource name '%s' has finished.\n",
      $offline_user_data_job_resource_name;
  } else {
    printf
      "Offline user data job with resource name '%s' still pending after %d " .
      "seconds, continuing the execution of the code example anyway.\n",
      $offline_user_data_job_resource_name,
      POLL_TIMEOUT_SECONDS;
  }
}