Upload Store Sales Conversions

The Google Ads API supports uploading store sales conversions that allow you to import offline transactions into Google Ads either directly or through third-party partners. By matching transaction data from your point-of-sale systems or customer database, you can see how your ads translate into offline purchases.

The first step is to create a conversion action using the Google Ads UI or the Google Ads API. Through the Google Ads API, set up a ConversionAction and specify the type attribute as STORE_SALES_DIRECT_UPLOAD or STORE_SALES. After creating the conversion action, you are ready to start uploading store sales conversions using either the UI or the Google Ads API.

Code example

Once you create the conversion action, you need to associate the store sales conversions with it by passing the conversion action resource name and store transactions into a UserData object and add it to a created OfflineUserDataJob resource through the OfflineUserDataJobService.

Java

// Copyright 2020 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.

package com.google.ads.googleads.examples.remarketing;

import com.beust.jcommander.Parameter;
import com.google.ads.googleads.examples.utils.ArgumentNames;
import com.google.ads.googleads.examples.utils.CodeSampleParams;
import com.google.ads.googleads.lib.GoogleAdsClient;
import com.google.ads.googleads.v6.common.OfflineUserAddressInfo;
import com.google.ads.googleads.v6.common.StoreSalesMetadata;
import com.google.ads.googleads.v6.common.StoreSalesThirdPartyMetadata;
import com.google.ads.googleads.v6.common.TransactionAttribute;
import com.google.ads.googleads.v6.common.UserData;
import com.google.ads.googleads.v6.common.UserIdentifier;
import com.google.ads.googleads.v6.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus;
import com.google.ads.googleads.v6.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType;
import com.google.ads.googleads.v6.errors.GoogleAdsError;
import com.google.ads.googleads.v6.errors.GoogleAdsException;
import com.google.ads.googleads.v6.resources.OfflineUserDataJob;
import com.google.ads.googleads.v6.services.AddOfflineUserDataJobOperationsRequest;
import com.google.ads.googleads.v6.services.AddOfflineUserDataJobOperationsResponse;
import com.google.ads.googleads.v6.services.CreateOfflineUserDataJobResponse;
import com.google.ads.googleads.v6.services.GoogleAdsRow;
import com.google.ads.googleads.v6.services.GoogleAdsServiceClient;
import com.google.ads.googleads.v6.services.OfflineUserDataJobOperation;
import com.google.ads.googleads.v6.services.OfflineUserDataJobServiceClient;
import com.google.ads.googleads.v6.utils.ResourceNames;
import com.google.common.collect.ImmutableList;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * Uploads offline data for store sales transactions.
 *
 * <p>This feature is only available to allowlisted accounts. See
 * https://support.google.com/google-ads/answer/7620302 for more details.
 */
public class UploadStoreSalesTransactions {

  private static class UploadStoreSalesTransactionsParams extends CodeSampleParams {

    @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true)
    private Long customerId;

    @Parameter(
        names = ArgumentNames.OFFLINE_USER_DATA_JOB_TYPE,
        required = false,
        description =
            "The type of user data in the job (first or third party). If you have an official"
                + " store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY."
                + " Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.")
    private OfflineUserDataJobType offlineUserDataJobType =
        OfflineUserDataJobType.STORE_SALES_UPLOAD_FIRST_PARTY;

    @Parameter(
        names = ArgumentNames.EXTERNAL_ID,
        description =
            "Optional (but recommended) external ID to identify the offline user data job")
    private Long externalId;

    @Parameter(
        names = ArgumentNames.CONVERSION_ACTION_ID,
        required = true,
        description = "The ID of a store sales conversion action")
    private Long conversionActionId;

    @Parameter(
        names = ArgumentNames.CUSTOM_KEY,
        required = false,
        description =
            "Only required after creating a custom key and custom values in the account."
                + " Custom key and values are used to segment store sales conversions."
                + " This measurement can be used to provide more advanced insights.")
    private String customKey;

    @Parameter(
        names = ArgumentNames.ADVERTISER_UPLOAD_DATE_TIME,
        description = "Only required if uploading third party data")
    private String advertiserUploadDateTime;

    @Parameter(
        names = ArgumentNames.BRIDGE_MAP_VERSION_ID,
        description = "Only required if uploading third party data")
    private String bridgeMapVersionId;

    @Parameter(
        names = ArgumentNames.PARTNER_ID,
        description = "Only required if uploading third party data")
    private Long partnerId;
  }

  public static void main(String[] args)
      throws InterruptedException, ExecutionException, TimeoutException,
          UnsupportedEncodingException {
    UploadStoreSalesTransactionsParams params = new UploadStoreSalesTransactionsParams();
    if (!params.parseArguments(args)) {

      // Either pass the required parameters for this example on the command line, or insert them
      // into the code here. See the parameter class definition above for descriptions.
      params.customerId = Long.parseLong("INSERT_CUSTOMER_ID_HERE");
      params.offlineUserDataJobType =
          OfflineUserDataJobType.valueOf("INSERT_OFFLINE_USER_DATA_JOB_TYPE_HERE");
      params.conversionActionId = Long.parseLong("INSERT_CONVERSION_ACTION_ID_HERE");
      // OPTIONAL (but recommended): Specify an external ID for the job.
      // params.externalId = Long.parseLong("INSERT_EXTERNAL_ID_HERE");
      // OPTIONAL: If uploading data with custom key and values, also specify the following value:
      // params.customKey = "INSERT_CUSTOM_KEY_HERE";
      // OPTIONAL: If uploading third party data, also specify the following values:
      // params.advertiserUploadDateTime = "INSERT_ADVERTISER_UPLOAD_DATE_TIME_HERE";
      // params.bridgeMapVersionId = "INSERT_BRIDGE_MAP_VERSION_ID_HERE";
      // params.partnerId = Long.parseLong("INSERT_PARTNER_ID_HERE");
    }

    GoogleAdsClient googleAdsClient = null;
    try {
      googleAdsClient = GoogleAdsClient.newBuilder().fromPropertiesFile().build();
    } catch (FileNotFoundException fnfe) {
      System.err.printf(
          "Failed to load GoogleAdsClient configuration from file. Exception: %s%n", fnfe);
      System.exit(1);
    } catch (IOException ioe) {
      System.err.printf("Failed to create GoogleAdsClient. Exception: %s%n", ioe);
      System.exit(1);
    }

    try {
      new UploadStoreSalesTransactions()
          .runExample(
              googleAdsClient,
              params.customerId,
              params.offlineUserDataJobType,
              params.externalId,
              params.conversionActionId,
              params.customKey,
              params.advertiserUploadDateTime,
              params.bridgeMapVersionId,
              params.partnerId);
    } catch (GoogleAdsException gae) {
      // GoogleAdsException is the base class for most exceptions thrown by an API request.
      // Instances of this exception have a message and a GoogleAdsFailure that contains a
      // collection of GoogleAdsErrors that indicate the underlying causes of the
      // GoogleAdsException.
      System.err.printf(
          "Request ID %s failed due to GoogleAdsException. Underlying errors:%n",
          gae.getRequestId());
      int i = 0;
      for (GoogleAdsError googleAdsError : gae.getGoogleAdsFailure().getErrorsList()) {
        System.err.printf("  Error %d: %s%n", i++, googleAdsError);
      }
      System.exit(1);
    }
  }

  /**
   * Runs the example.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param offlineUserDataJobType the type of offline user data in the job (first party or third
   *     party). If you have an official store sales partnership with Google, use {@code
   *     STORE_SALES_UPLOAD_THIRD_PARTY}. Otherwise, use {@code STORE_SALES_UPLOAD_FIRST_PARTY}.
   * @param externalId optional (but recommended) external ID for the offline user data job.
   * @param conversionActionId the ID of a store sales conversion action.
   * @param customKey to segment store sales conversions. Only required after creating a custom key
   *     and custom values in the account.
   * @param advertiserUploadDateTime date and time the advertiser uploaded data to the partner. Only
   *     required for third party uploads.
   * @param bridgeMapVersionId version of partner IDs to be used for uploads. Only required for
   *     third party uploads.
   * @param partnerId ID of the third party partner. Only required for third party uploads.
   * @throws GoogleAdsException if an API request failed with one or more service errors.
   */
  private void runExample(
      GoogleAdsClient googleAdsClient,
      long customerId,
      OfflineUserDataJobType offlineUserDataJobType,
      Long externalId,
      long conversionActionId,
      String customKey,
      String advertiserUploadDateTime,
      String bridgeMapVersionId,
      Long partnerId)
      throws InterruptedException, ExecutionException, TimeoutException,
          UnsupportedEncodingException {
    String offlineUserDataJobResourceName;
    try (OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
        googleAdsClient.getLatestVersion().createOfflineUserDataJobServiceClient()) {
      // Creates an offline user data job for uploading transactions.
      offlineUserDataJobResourceName =
          createOfflineUserDataJob(
              offlineUserDataJobServiceClient,
              customerId,
              offlineUserDataJobType,
              externalId,
              customKey,
              advertiserUploadDateTime,
              bridgeMapVersionId,
              partnerId);

      // Adds transactions to the job.
      addTransactionsToOfflineUserDataJob(
          offlineUserDataJobServiceClient,
          customerId,
          offlineUserDataJobResourceName,
          conversionActionId);

      // Issues an asynchronous request to run the offline user data job.
      offlineUserDataJobServiceClient.runOfflineUserDataJobAsync(offlineUserDataJobResourceName);

      System.out.printf(
          "Sent request to asynchronously run offline user data job: %s%n",
          offlineUserDataJobResourceName);
    }

    // Offline user data jobs may take up to 24 hours to complete, so instead of waiting for the job
    // to complete, retrieves and displays the job status once and then prints the query to use to
    // check the job again later.
    checkJobStatus(googleAdsClient, customerId, offlineUserDataJobResourceName);
  }

  /**
   * Creates an offline user data job for uploading store sales transactions.
   *
   * @return the resource name of the created job.
   */
  private String createOfflineUserDataJob(
      OfflineUserDataJobServiceClient offlineUserDataJobServiceClient,
      long customerId,
      OfflineUserDataJobType offlineUserDataJobType,
      Long externalId,
      String customKey,
      String advertiserUploadDateTime,
      String bridgeMapVersionId,
      Long partnerId) {
    // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses the
    // term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is called
    // loyaltyFraction in the Google Ads API.
    StoreSalesMetadata.Builder storeSalesMetadataBuilder =
        // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
        // details.
        StoreSalesMetadata.newBuilder()
            // Sets the fraction of your overall sales that you (or the advertiser, in the third
            // party case) can associate with a customer (email, phone number, address, etc.) in
            // your database or loyalty program.
            // For example, set this to 0.7 if you have 100 transactions over 30 days, and out of
            // those 100 transactions, you can identify 70 by an email address or phone number.
            .setLoyaltyFraction(0.7)
            // Sets the fraction of sales you're uploading out of the overall sales that you (or the
            // advertiser, in the third party case) can associate with a customer. In most cases,
            // you will set this to 1.0.
            // Continuing the example above for loyalty fraction, a value of 1.0 here indicates that
            // you are uploading all 70 of the transactions that can be identified by an email
            // address or phone number.
            .setTransactionUploadFraction(1.0);

    if (customKey != null && !customKey.isEmpty()) {
      storeSalesMetadataBuilder.setCustomKey(customKey);
    }

    if (OfflineUserDataJobType.STORE_SALES_UPLOAD_THIRD_PARTY == offlineUserDataJobType) {
      // Creates additional metadata required for uploading third party data.
      StoreSalesThirdPartyMetadata storeSalesThirdPartyMetadata =
          StoreSalesThirdPartyMetadata.newBuilder()
              // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
              .setAdvertiserUploadDateTime(advertiserUploadDateTime)

              // Sets the fraction of transactions you received from the advertiser that have valid
              // formatting and values. This captures any transactions the advertiser provided to
              // you but which you are unable to upload to Google due to formatting errors or
              // missing data.
              // In most cases, you will set this to 1.0.
              .setValidTransactionFraction(1.0)
              // Sets the fraction of valid transactions (as defined above) you received from the
              // advertiser that you (the third party) have matched to an external user ID on your
              // side.
              // In most cases, you will set this to 1.0.
              .setPartnerMatchFraction(1.0)

              // Sets the fraction of transactions you (the third party) are uploading out of the
              // transactions you received from the advertiser that meet both of the following
              // criteria:
              // 1. Are valid in terms of formatting and values. See valid transaction fraction
              // above.
              // 2. You matched to an external user ID on your side. See partner match fraction
              // above.
              // In most cases, you will set this to 1.0.
              .setPartnerUploadFraction(1.0)

              // Please speak with your Google representative to get the values to use for the
              // bridge map version and partner IDs.

              // Sets the version of partner IDs to be used for uploads.
              .setBridgeMapVersionId(bridgeMapVersionId)
              // Sets the third party partner ID uploading the transactions.
              .setPartnerId(partnerId)
              .build();
      storeSalesMetadataBuilder.setThirdPartyMetadata(storeSalesThirdPartyMetadata);
    }

    // Creates a new offline user data job.
    OfflineUserDataJob.Builder offlineUserDataJobBuilder =
        OfflineUserDataJob.newBuilder()
            .setType(offlineUserDataJobType)
            .setStoreSalesMetadata(storeSalesMetadataBuilder);
    if (externalId != null) {
      offlineUserDataJobBuilder.setExternalId(externalId);
    }

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

  /** Adds operations to the job for a set of sample transactions. */
  private void addTransactionsToOfflineUserDataJob(
      OfflineUserDataJobServiceClient offlineUserDataJobServiceClient,
      long customerId,
      String offlineUserDataJobResourceName,
      long conversionActionId)
      throws InterruptedException, ExecutionException, TimeoutException,
          UnsupportedEncodingException {
    // Constructs the operation for each transaction.
    List<OfflineUserDataJobOperation> userDataJobOperations =
        buildOfflineUserDataJobOperations(customerId, conversionActionId);

    // Issues a request to add the operations to the offline user data job.
    AddOfflineUserDataJobOperationsResponse response =
        offlineUserDataJobServiceClient.addOfflineUserDataJobOperations(
            AddOfflineUserDataJobOperationsRequest.newBuilder()
                .setResourceName(offlineUserDataJobResourceName)
                .setEnablePartialFailure(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());
    }
  }

  /**
   * Creates a list of offline user data job operations for sample transactions.
   *
   * @return a list of operations.
   */
  private List<OfflineUserDataJobOperation> buildOfflineUserDataJobOperations(
      long customerId, long conversionActionId) throws UnsupportedEncodingException {
    MessageDigest sha256Digest;
    try {
      // Gets a digest for generating hashed values using SHA-256. You must normalize and hash the
      // the value for any field where the name begins with "hashed". See the normalizeAndHash()
      // method.
      sha256Digest = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("Missing SHA-256 algorithm implementation", e);
    }

    // Create the first transaction for upload based on an email address and state.
    UserData userDataWithEmailAddress =
        UserData.newBuilder()
            .addAllUserIdentifiers(
                ImmutableList.of(
                    UserIdentifier.newBuilder()
                        .setHashedEmail(
                            // Email addresses must be normalized and hashed.
                            normalizeAndHash(sha256Digest, "customer@example.com"))
                        .build(),
                    UserIdentifier.newBuilder()
                        .setAddressInfo(OfflineUserAddressInfo.newBuilder().setState("NY"))
                        .build()))
            .setTransactionAttribute(
                TransactionAttribute.newBuilder()
                    .setConversionAction(
                        ResourceNames.conversionAction(customerId, conversionActionId))
                    .setCurrencyCode("USD")
                    // Converts the transaction amount from $200 USD to micros.
                    .setTransactionAmountMicros(200L * 1_000_000L)
                    // Specifies the date and time of the transaction. The format is
                    // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                    // timezone offset from UTC. If the offset is absent, the API will
                    // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                    // or "2018-02-01 14:34:30+03:00".
                    .setTransactionDateTime("2020-05-01 23:52:12")
                // OPTIONAL: If uploading data with custom key and values, also specify the
                // following value:
                // .setCustomValue("INSERT_CUSTOM_VALUE_HERE")
                )
            .build();

    // Creates the second transaction for upload based on a physical address.
    UserData userDataWithPhysicalAddress =
        UserData.newBuilder()
            .addUserIdentifiers(
                UserIdentifier.newBuilder()
                    .setAddressInfo(
                        OfflineUserAddressInfo.newBuilder()
                            .setHashedFirstName(normalizeAndHash(sha256Digest, "John"))
                            .setHashedLastName(normalizeAndHash(sha256Digest, "Doe"))
                            .setCountryCode("US")
                            .setPostalCode("10011")))
            .setTransactionAttribute(
                TransactionAttribute.newBuilder()
                    .setConversionAction(
                        ResourceNames.conversionAction(customerId, conversionActionId))
                    .setCurrencyCode("EUR")
                    // Converts the transaction amount from 450 EUR to micros.
                    .setTransactionAmountMicros(450L * 1_000_000L)
                    // Specifies the date and time of the transaction. This date and time will be
                    // interpreted by the API using the Google Ads customer's time zone.
                    // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                    .setTransactionDateTime("2020-05-14 19:07:02")
                // OPTIONAL: If uploading data with custom key and values, also specify the
                // following value:
                // .setCustomValue("INSERT_CUSTOM_VALUE_HERE")
                )
            .build();

    // Creates the operations to add the two transactions.
    List<OfflineUserDataJobOperation> operations = new ArrayList<>();
    for (UserData userData : Arrays.asList(userDataWithEmailAddress, userDataWithPhysicalAddress)) {
      operations.add(OfflineUserDataJobOperation.newBuilder().setCreate(userData).build());
    }

    return operations;
  }

  /**
   * Returns the result of normalizing and then hashing the string using the provided digest.
   * Private customer data must be hashed during upload, as described at
   * https://support.google.com/google-ads/answer/7506124.
   *
   * @param digest the digest to use to hash the normalized string.
   * @param s the string to normalize and hash.
   */
  private String normalizeAndHash(MessageDigest digest, String s)
      throws UnsupportedEncodingException {
    // Normalizes by removing leading and trailing whitespace and converting all characters to
    // lower case.
    String normalized = s.trim().toLowerCase();
    // Hashes the normalized string using the hashing algorithm.
    byte[] hash = digest.digest(normalized.getBytes("UTF-8"));
    StringBuilder result = new StringBuilder();
    for (byte b : hash) {
      result.append(String.format("%02x", b));
    }

    return result.toString();
  }

  /** Retrieves, checks, and prints the status of the offline user data job. */
  private void checkJobStatus(
      GoogleAdsClient googleAdsClient, long customerId, String offlineUserDataJobResourceName) {
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      String query =
          String.format(
              "SELECT offline_user_data_job.resource_name, "
                  + "offline_user_data_job.id, "
                  + "offline_user_data_job.status, "
                  + "offline_user_data_job.type, "
                  + "offline_user_data_job.failure_reason "
                  + "FROM offline_user_data_job "
                  + "WHERE offline_user_data_job.resource_name = '%s'",
              offlineUserDataJobResourceName);
      // Issues the query and gets the GoogleAdsRow containing the job from the response.
      GoogleAdsRow googleAdsRow =
          googleAdsServiceClient
              .search(Long.toString(customerId), query)
              .iterateAll()
              .iterator()
              .next();
      OfflineUserDataJob offlineUserDataJob = googleAdsRow.getOfflineUserDataJob();
      System.out.printf(
          "Offline user data job ID %d with type '%s' has status: %s%n",
          offlineUserDataJob.getId(), offlineUserDataJob.getType(), offlineUserDataJob.getStatus());
      OfflineUserDataJobStatus jobStatus = offlineUserDataJob.getStatus();
      if (OfflineUserDataJobStatus.FAILED == jobStatus) {
        System.out.printf("  Failure reason: %s%n", offlineUserDataJob.getFailureReason());
      } else if (OfflineUserDataJobStatus.PENDING == jobStatus
          || OfflineUserDataJobStatus.RUNNING == jobStatus) {
        System.out.println();
        System.out.printf(
            "To check the status of the job periodically, use the following GAQL query with"
                + " GoogleAdsService.search:%n%s%n",
            query);
      }
    }
  }
}

C#

// Copyright 2020 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
//
//     http://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.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.V6.Common;
using Google.Ads.GoogleAds.V6.Errors;
using Google.Ads.GoogleAds.V6.Resources;
using Google.Ads.GoogleAds.V6.Services;
using static Google.Ads.GoogleAds.V6.Enums.OfflineUserDataJobTypeEnum.Types;
using static Google.Ads.GoogleAds.V6.Enums.OfflineUserDataJobStatusEnum.Types;

namespace Google.Ads.GoogleAds.Examples.V6
{
    /// <summary>
    /// This code example uploads offline data for store sales transactions.
    /// This feature is only available to allowlisted accounts. See
    /// https://support.google.com/google-ads/answer/7620302 for more details.
    /// </summary>
    public class UploadStoreSalesTransactions : ExampleBase
    {
        // Gets a digest for generating hashed values using SHA-256. You must normalize and hash the
        // the value for any field where the name begins with "hashed". See the normalizeAndHash()
        // method.
        private static readonly SHA256 _digest = SHA256.Create();

        // If uploading data with custom key and values, specify the value:
        private const string CUSTOM_VALUE = "INSERT_CUSTOM_VALUE_HERE";

        /// <summary>
        /// Main method, to run this code example as a standalone application.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        public static void Main(string[] args)
        {
            UploadStoreSalesTransactions codeExample = new UploadStoreSalesTransactions();

            Console.WriteLine(codeExample.Description);

            // The Google Ads customer ID for which the call is made.
            long customerId = long.Parse("INSERT_CUSTOMER_ID_HERE");

            // The type of user data in the job (first or third party). If you have an official
            // store sales partnership with Google, use StoreSalesUploadThirdParty.
            // Otherwise, use StoreSalesUploadFirstParty or omit this parameter.
            OfflineUserDataJobType offlineUserDataJobType =
                OfflineUserDataJobType.StoreSalesUploadFirstParty;

            // The ID of a store sales conversion action.
            long conversionActionId = long.Parse("INSERT_CONVERSION_ACTION_ID_HERE");

            // Optional (but recommended) external ID to identify the offline user data job.
            long? externalId = long.Parse("INSERT_EXTERNAL_ID_HERE");

            // OPTIONAL: If uploading third party data, also specify the following values:
            string advertiserUploadDateTime = "INSERT_ADVERTISER_UPLOAD_DATE_TIME_HERE";
            string bridgeMapVersionId = "INSERT_BRIDGE_MAP_VERSION_ID_HERE";
            long? partnerId = long.Parse("INSERT_PARTNER_ID_HERE");

            // OPTIONAL: If uploading data with custom key and values, specify the key:
            string customKey = "INSERT_CUSTOM_KEY_HERE";

            codeExample.Run(new GoogleAdsClient(), customerId, conversionActionId,
                offlineUserDataJobType: offlineUserDataJobType,
                externalId: externalId,
                advertiserUploadDateTime: advertiserUploadDateTime,
                bridgeMapVersionId: bridgeMapVersionId,
                partnerId: partnerId,
                customKey: customKey);
        }

        /// <summary>
        /// Returns a description about the code example.
        /// </summary>
        public override string Description =>
            "This code example uploads offline data for store sales transactions. This feature " +
            "is only available to allowlisted accounts. See " +
            "https://support.google.com/google-ads/answer/7620302 for more details.";

        /// <summary>
        /// Runs the code example.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="offlineUserDataJobType">The type of user data in the job (first or third
        ///     party). If you have an official store sales partnership with Google, use
        ///     StoreSalesUploadThirdParty. Otherwise, use StoreSalesUploadFirstParty or
        ///     omit this parameter.</param>
        /// <param name="externalId">Optional (but recommended) external ID to identify the offline
        ///     user data job.</param>
        /// <param name="advertiserUploadDateTime">Date and time the advertiser uploaded data to the
        ///     partner. Only required if uploading third party data.</param>
        /// <param name="bridgeMapVersionId">Version of partner IDs to be used for uploads. Only
        ///     required if uploading third party data.</param>
        /// <param name="partnerId">ID of the third party partner. Only required if uploading third
        ///     party data.</param>
        /// <param name="customKey">Optional custom key name. Only required if uploading data
        ///     with custom key and values.</param>
        public void Run(GoogleAdsClient client, long customerId, long conversionActionId,
            OfflineUserDataJobType offlineUserDataJobType =
                OfflineUserDataJobType.StoreSalesUploadFirstParty,
            long? externalId = null, string advertiserUploadDateTime = null,
            string bridgeMapVersionId = null, long? partnerId = null, string customKey=null)
        {
            // Get the OfflineUserDataJobServiceClient.
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
                client.GetService(Services.V6.OfflineUserDataJobService);

            // Ensure that a valid job type is provided.
            if (offlineUserDataJobType != OfflineUserDataJobType.StoreSalesUploadFirstParty &
                offlineUserDataJobType != OfflineUserDataJobType.StoreSalesUploadThirdParty)
            {
                Console.WriteLine("Invalid job type specified, defaulting to First Party.");
                offlineUserDataJobType = OfflineUserDataJobType.StoreSalesUploadFirstParty;
            }

            try
            {
                // Creates an offline user data job for uploading transactions.
                string offlineUserDataJobResourceName =
                    CreateOfflineUserDataJob(offlineUserDataJobServiceClient, customerId,
                        offlineUserDataJobType, externalId, advertiserUploadDateTime,
                        bridgeMapVersionId, partnerId, customKey);

                // Adds transactions to the job.
                AddTransactionsToOfflineUserDataJob(offlineUserDataJobServiceClient, customerId,
                    offlineUserDataJobResourceName, conversionActionId, customKey);

                // Issues an asynchronous request to run the offline user data job.
                offlineUserDataJobServiceClient.RunOfflineUserDataJobAsync(
                    offlineUserDataJobResourceName);

                Console.WriteLine("Sent request to asynchronously run offline user data job " +
                    $"{offlineUserDataJobResourceName}.");

                // Offline user data jobs may take up to 24 hours to complete, so instead of waiting
                // for the job to complete, retrieves and displays the job status once and then
                // prints the query to use to check the job again later.
                CheckJobStatus(client, customerId, offlineUserDataJobResourceName);
            }
            catch (GoogleAdsException e)
            {
                Console.WriteLine("Failure:");
                Console.WriteLine($"Message: {e.Message}");
                Console.WriteLine($"Failure: {e.Failure}");
                Console.WriteLine($"Request ID: {e.RequestId}");
                throw;
            }
        }

        /// <summary>
        /// Creates an offline user data job for uploading store sales transactions.
        /// </summary>
        /// <param name="offlineUserDataJobServiceClient">The offline user data job service
        ///     client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobType">The type of user data in the job (first or third
        ///     party). If you have an official store sales partnership with Google, use
        ///     StoreSalesUploadThirdParty. Otherwise, use StoreSalesUploadFirstParty or
        ///     omit this parameter.</param>
        /// <param name="externalId">Optional (but recommended) external ID to identify the offline
        ///     user data job.</param>
        /// <param name="advertiserUploadDateTime">Date and time the advertiser uploaded data to the
        ///     partner. Only required if uploading third party data.</param>
        /// <param name="bridgeMapVersionId">Version of partner IDs to be used for uploads. Only
        ///     required if uploading third party data.</param>
        /// <param name="partnerId">ID of the third party partner. Only required if uploading third
        ///     party data.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        ///     and value.</param>
        /// <returns>The resource name of the created job.</returns>
        private string CreateOfflineUserDataJob(
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId,
            OfflineUserDataJobType offlineUserDataJobType, long? externalId,
            string advertiserUploadDateTime, string bridgeMapVersionId, long? partnerId,
            string customKey)
        {
            // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses
            // the term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is
            // called loyaltyFraction in the Google Ads API.

            // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
            // details.
            StoreSalesMetadata storeSalesMetadata = new StoreSalesMetadata()
            {
                // Sets the fraction of your overall sales that you (or the advertiser, in the third
                // party case) can associate with a customer (email, phone number, address, etc.) in
                // your database or loyalty program.
                // For example, set this to 0.7 if you have 100 transactions over 30 days, and out
                // of those 100 transactions, you can identify 70 by an email address or phone
                // number.
                LoyaltyFraction = 0.7,
                // Sets the fraction of sales you're uploading out of the overall sales that you (or
                // the advertiser, in the third party case) can associate with a customer. In most
                // cases, you will set this to 1.0.
                // Continuing the example above for loyalty fraction, a value of 1.0 here indicates
                // that you are uploading all 70 of the transactions that can be identified by an
                // email address or phone number.
                TransactionUploadFraction = 1.0
            };

            // Apply the custom key if provided.
            if (!string.IsNullOrEmpty(customKey))
            {
                storeSalesMetadata.CustomKey = customKey;
            }

            // Creates additional metadata required for uploading third party data.
            if (offlineUserDataJobType == OfflineUserDataJobType.StoreSalesUploadThirdParty)
            {
                StoreSalesThirdPartyMetadata storeSalesThirdPartyMetadata =
                    new StoreSalesThirdPartyMetadata()
                    {
                        // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                        AdvertiserUploadDateTime = advertiserUploadDateTime,

                        // Sets the fraction of transactions you received from the advertiser that
                        // have valid formatting and values. This captures any transactions the
                        // advertiser provided to you but which you are unable to upload to Google
                        // due to formatting errors or missing data.
                        // In most cases, you will set this to 1.0.
                        ValidTransactionFraction = 1.0,

                        // Sets the fraction of valid transactions (as defined above) you received
                        // from the advertiser that you (the third party) have matched to an
                        // external user ID on your side.
                        // In most cases, you will set this to 1.0.
                        PartnerMatchFraction = 1.0,

                        // Sets the fraction of transactions you (the third party) are uploading out
                        // of the transactions you received from the advertiser that meet both of
                        // the following criteria:
                        // 1. Are valid in terms of formatting and values. See valid transaction
                        // fraction above.
                        // 2. You matched to an external user ID on your side. See partner match
                        // fraction above.
                        // In most cases, you will set this to 1.0.
                        PartnerUploadFraction = 1.0,

                        // Sets the version of partner IDs to be used for uploads.
                        // Please speak with your Google representative to get the values to use for
                        // the bridge map version and partner IDs.
                        BridgeMapVersionId = bridgeMapVersionId,
                    };

                // Sets the third party partner ID uploading the transactions.
                if (partnerId.HasValue)
                {
                    storeSalesThirdPartyMetadata.PartnerId = partnerId.Value;
                }
                storeSalesMetadata.ThirdPartyMetadata = storeSalesThirdPartyMetadata;
            }

            // Creates a new offline user data job.
            OfflineUserDataJob offlineUserDataJob = new OfflineUserDataJob()
            {
                Type = offlineUserDataJobType,
                StoreSalesMetadata = storeSalesMetadata
            };

            if (externalId.HasValue)
            {
                offlineUserDataJob.ExternalId = externalId.Value;
            }

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

        /// <summary>
        /// Adds operations to a job for a set of sample transactions.
        /// </summary>
        /// <param name="offlineUserDataJobServiceClient">The offline user data job service
        ///     client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobResourceName">The resource name of the job to which to
        ///     add transactions.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        ///     and value.</param>
        private void AddTransactionsToOfflineUserDataJob(
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId,
            string offlineUserDataJobResourceName, long conversionActionId, string customKey)
        {
            // Constructions an operation for each transaction.
            List<OfflineUserDataJobOperation> userDataJobOperations =
                BuildOfflineUserDataJobOperations(customerId, conversionActionId, customKey);

            // Issues a request with partial failure enabled to add the operations to the offline
            // user data job.
            AddOfflineUserDataJobOperationsResponse response = offlineUserDataJobServiceClient
                .AddOfflineUserDataJobOperations(new AddOfflineUserDataJobOperationsRequest()
                {
                    EnablePartialFailure = true,
                    ResourceName = offlineUserDataJobResourceName,
                    Operations = {userDataJobOperations}
                });

            // 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 (response.PartialFailureError != null)
            {
                Console.WriteLine($"Encountered {response.PartialFailureError.Details.Count} " +
                    $"partial failure errors while adding {userDataJobOperations.Count} " +
                    "operations to the offline user data job: " +
                    $"'{response.PartialFailureError.Message}'. Only the successfully added " +
                    "operations will be executed when the job runs.");
            }
            else
            {
                Console.WriteLine($"Successfully added {userDataJobOperations.Count} operations " +
                    "to the offline user data job.");
            }
        }

        /// <summary>
        /// Creates a list of offline user data job operations for sample transactions.
        /// </summary>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        ///     and value.</param>
        /// <returns>A list of operations.</returns>
        private List<OfflineUserDataJobOperation> BuildOfflineUserDataJobOperations(long customerId,
            long conversionActionId, string customKey)
        {
            // Create the first transaction for upload based on an email address and state.
            UserData userDataWithEmailAddress = new UserData()
            {
                UserIdentifiers =
                {
                    new UserIdentifier()
                    {
                        // Email addresses must be normalized and hashed.
                        HashedEmail = NormalizeAndHash("customer@example.com")
                    },
                    new UserIdentifier()
                    {
                        AddressInfo = new OfflineUserAddressInfo()
                        {
                            State = "NY"
                        }
                    },
                },
                TransactionAttribute = new TransactionAttribute()
                {
                    ConversionAction =
                        ResourceNames.ConversionAction(customerId, conversionActionId),
                    CurrencyCode = "USD",
                    // Converts the transaction amount from $200 USD to micros.
                    TransactionAmountMicros = 200L * 1_000_000L,
                    // Specifies the date and time of the transaction. The format is 
                    // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                    // timezone offset from UTC. If the offset is absent, the API will
                    // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                    // or "2018-02-01 14:34:30+03:00".                 
                    TransactionDateTime = DateTime.Today.AddDays(-2).ToString("yyyy-MM-dd HH:mm:ss")
                }
            };

            // Set the custom value if a custom key was provided.
            if (!string.IsNullOrEmpty(customKey))
            {
                userDataWithEmailAddress.TransactionAttribute.CustomValue = CUSTOM_VALUE;
            }

            // Creates the second transaction for upload based on a physical address.
            UserData userDataWithPhysicalAddress = new UserData()
            {
                UserIdentifiers =
                {
                    new UserIdentifier()
                    {
                        AddressInfo = new OfflineUserAddressInfo()
                        {
                            // Names must be normalized and hashed.
                            HashedFirstName = NormalizeAndHash("John"),
                            HashedLastName = NormalizeAndHash("Doe"),
                            CountryCode = "US",
                            PostalCode = "10011"
                        }
                    }
                },
                TransactionAttribute = new TransactionAttribute()
                {
                    ConversionAction =
                        ResourceNames.ConversionAction(customerId, conversionActionId),
                    CurrencyCode = "EUR",
                    // Converts the transaction amount from 450 EUR to micros.
                    TransactionAmountMicros = 450L * 1_000_000L,
                    // Specifies the date and time of the transaction. This date and time will be
                    // interpreted by the API using the Google Ads customer's time zone.
                    // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                    // e.g. "2020-05-14 19:07:02".
                    TransactionDateTime = DateTime.Today.AddDays(-1).ToString("yyyy-MM-dd HH:mm:ss")
                }
            };

            // Creates the operations to add the two transactions.
            List<OfflineUserDataJobOperation> operations = new List<OfflineUserDataJobOperation>()
            {
                new OfflineUserDataJobOperation()
                {
                    Create = userDataWithEmailAddress
                },
                new OfflineUserDataJobOperation()
                {
                    Create = userDataWithPhysicalAddress
                }
            };

            return operations;
        }

        /// <summary>
        /// Normalizes and hashes a string value.
        /// </summary>
        /// <param name="value">The value to normalize and hash.</param>
        /// <returns>The normalized and hashed value.</returns>
        private static string NormalizeAndHash(string value)
        {
            return ToSha256String(_digest, ToNormalizedValue(value));
        }

        /// <summary>
        /// Hash a string value using SHA-256 hashing algorithm.
        /// </summary>
        /// <param name="digest">Provides the algorithm for SHA-256.</param>
        /// <param name="value">The string value (e.g. an email address) to hash.</param>
        /// <returns>The hashed value.</returns>
        private static string ToSha256String(SHA256 digest, string value)
        {
            byte[] digestBytes = digest.ComputeHash(Encoding.UTF8.GetBytes(value));
            // Convert the byte array into an unhyphenated hexadecimal string.
            return BitConverter.ToString(digestBytes).Replace("-", string.Empty);
        }

        /// <summary>
        /// Removes leading and trailing whitespace and converts all characters to
        /// lower case.
        /// </summary>
        /// <param name="value">The value to normalize.</param>
        /// <returns>The normalized value.</returns>
        private static string ToNormalizedValue(string value)
        {
            return value.Trim().ToLower();
        }

        /// <summary>
        /// Retrieves, checks, and prints the status of the offline user data job.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobResourceName">The resource name of the job whose status
        ///     you wish to check.</param>
        private void CheckJobStatus(GoogleAdsClient client, long customerId,
            string offlineUserDataJobResourceName)
        {
            GoogleAdsServiceClient googleAdsServiceClient =
                client.GetService(Services.V6.GoogleAdsService);

            string query = $@"SELECT offline_user_data_job.resource_name,
                    offline_user_data_job.id,
                    offline_user_data_job.status,
                    offline_user_data_job.type,
                    offline_user_data_job.failure_reason
                FROM offline_user_data_job
                WHERE offline_user_data_job.resource_name = '{offlineUserDataJobResourceName}'";

            // Issues the query and gets the GoogleAdsRow containing the job from the response.
            GoogleAdsRow googleAdsRow = googleAdsServiceClient.Search(
                customerId.ToString(), query).First();

            OfflineUserDataJob offlineUserDataJob = googleAdsRow.OfflineUserDataJob;

            OfflineUserDataJobStatus jobStatus = offlineUserDataJob.Status;
            Console.WriteLine($"Offline user data job ID {offlineUserDataJob.Id} with type " +
                $"'{offlineUserDataJob.Type}' has status {offlineUserDataJob.Status}.");

            if (jobStatus == OfflineUserDataJobStatus.Failed)
            {
                Console.WriteLine($"\tFailure reason: {offlineUserDataJob.FailureReason}");
            }
            else if (jobStatus == OfflineUserDataJobStatus.Pending |
                     jobStatus == OfflineUserDataJobStatus.Running)
            {
                Console.WriteLine("\nTo check the status of the job periodically, use the" +
                    $"following GAQL query with GoogleAdsService.Search:\n{query}\n");
            }
        }
    }
}

PHP

<?php

/**
 * Copyright 2020 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.
 */

namespace Google\Ads\GoogleAds\Examples\Remarketing;

require __DIR__ . '/../../vendor/autoload.php';

use GetOpt\GetOpt;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentNames;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentParser;
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
use Google\Ads\GoogleAds\Lib\V6\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V6\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\V6\GoogleAdsException;
use Google\Ads\GoogleAds\Lib\V6\GoogleAdsServerStreamDecorator;
use Google\Ads\GoogleAds\Util\V6\ResourceNames;
use Google\Ads\GoogleAds\V6\Common\OfflineUserAddressInfo;
use Google\Ads\GoogleAds\V6\Common\StoreSalesMetadata;
use Google\Ads\GoogleAds\V6\Common\StoreSalesThirdPartyMetadata;
use Google\Ads\GoogleAds\V6\Common\TransactionAttribute;
use Google\Ads\GoogleAds\V6\Common\UserData;
use Google\Ads\GoogleAds\V6\Common\UserIdentifier;
use Google\Ads\GoogleAds\V6\Enums\OfflineUserDataJobFailureReasonEnum\OfflineUserDataJobFailureReason;
use Google\Ads\GoogleAds\V6\Enums\OfflineUserDataJobStatusEnum\OfflineUserDataJobStatus;
use Google\Ads\GoogleAds\V6\Enums\OfflineUserDataJobTypeEnum\OfflineUserDataJobType;
use Google\Ads\GoogleAds\V6\Errors\GoogleAdsError;
use Google\Ads\GoogleAds\V6\Resources\OfflineUserDataJob;
use Google\Ads\GoogleAds\V6\Services\AddOfflineUserDataJobOperationsResponse;
use Google\Ads\GoogleAds\V6\Services\CreateOfflineUserDataJobResponse;
use Google\Ads\GoogleAds\V6\Services\GoogleAdsRow;
use Google\Ads\GoogleAds\V6\Services\OfflineUserDataJobOperation;
use Google\Ads\GoogleAds\V6\Services\OfflineUserDataJobServiceClient;
use Google\ApiCore\ApiException;

/**
 * Uploads offline data for store sales transactions.
 *
 * This feature is only available to allowlisted accounts. See
 * https://support.google.com/google-ads/answer/7620302 for more details.
 */
class UploadStoreSalesTransactions
{
    private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE';

    /**
     * The type of user data in the job (first or third party). If you have an official
     * store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY.
     * Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.
     */
    private const OFFLINE_USER_DATA_JOB_TYPE = 'STORE_SALES_UPLOAD_FIRST_PARTY';
    /** The ID of a store sales conversion action. */
    private const CONVERSION_ACTION_ID = 'INSERT_CONVERSION_ACTION_ID_HERE';
    // Optional (but recommended) external ID to identify the offline user data job.
    /** The external ID for the offline user data job. */
    private const EXTERNAL_ID = null;

    // Optional: If uploading third party data, also specify the following values:
    /**
     * The date and time the advertiser uploaded data to the partner.
     * The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
     */
    private const ADVERTISER_UPLOAD_DATE_TIME = null;
    /** The version of partner IDs to be used for uploads. */
    private const BRIDGE_MAP_VERSION_ID = null;
    /** The ID of the third party partner. */
    private const PARTNER_ID = null;

    public static function main()
    {
        // Either pass the required parameters for this example on the command line, or insert them
        // into the constants above.
        $options = (new ArgumentParser())->parseCommandArguments([
            ArgumentNames::CUSTOMER_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::OFFLINE_USER_DATA_JOB_TYPE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::CONVERSION_ACTION_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::EXTERNAL_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::ADVERTISER_UPLOAD_DATE_TIME => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::BRIDGE_MAP_VERSION_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::PARTNER_ID => GetOpt::OPTIONAL_ARGUMENT,
        ]);

        // Generate a refreshable OAuth2 credential for authentication.
        $oAuth2Credential = (new OAuth2TokenBuilder())->fromFile()->build();

        // Construct a Google Ads client configured from a properties file and the
        // OAuth2 credentials above.
        $googleAdsClient = (new GoogleAdsClientBuilder())
            ->fromFile()
            ->withOAuth2Credential($oAuth2Credential)
            ->build();

        try {
            self::runExample(
                $googleAdsClient,
                $options[ArgumentNames::CUSTOMER_ID] ?: self::CUSTOMER_ID,
                $options[ArgumentNames::OFFLINE_USER_DATA_JOB_TYPE]
                    ?: self::OFFLINE_USER_DATA_JOB_TYPE,
                $options[ArgumentNames::CONVERSION_ACTION_ID] ?: self::CONVERSION_ACTION_ID,
                $options[ArgumentNames::EXTERNAL_ID] ?: self::EXTERNAL_ID,
                $options[ArgumentNames::ADVERTISER_UPLOAD_DATE_TIME]
                    ?: self::ADVERTISER_UPLOAD_DATE_TIME,
                $options[ArgumentNames::BRIDGE_MAP_VERSION_ID] ?: self::BRIDGE_MAP_VERSION_ID,
                $options[ArgumentNames::PARTNER_ID] ?: self::PARTNER_ID
            );
        } catch (GoogleAdsException $googleAdsException) {
            printf(
                "Request with ID '%s' has failed.%sGoogle Ads failure details:%s",
                $googleAdsException->getRequestId(),
                PHP_EOL,
                PHP_EOL
            );
            foreach ($googleAdsException->getGoogleAdsFailure()->getErrors() as $error) {
                /** @var GoogleAdsError $error */
                printf(
                    "\t%s: %s%s",
                    $error->getErrorCode()->getErrorCode(),
                    $error->getMessage(),
                    PHP_EOL
                );
            }
            exit(1);
        } catch (ApiException $apiException) {
            printf(
                "ApiException was thrown with message '%s'.%s",
                $apiException->getMessage(),
                PHP_EOL
            );
            exit(1);
        }
    }

    /**
     * Runs the example.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string|null $offlineUserDataJobType the type of offline user data in the job (first
     *     party or third party). If you have an official store sales partnership with Google, use
     *     `STORE_SALES_UPLOAD_THIRD_PARTY`. Otherwise, use `STORE_SALES_UPLOAD_FIRST_PARTY`
     * @param int $conversionActionId the ID of a store sales conversion action
     * @param int|null $externalId optional (but recommended) external ID for the offline user data
     *     job
     * @param string|null $advertiserUploadDateTime date and time the advertiser uploaded data to
     *     the partner. Only required for third party uploads
     * @param string|null $bridgeMapVersionId version of partner IDs to be used for uploads. Only
     *     required for third party uploads
     * @param int|null $partnerId ID of the third party partner. Only required for third party
     *     uploads
     */
    public static function runExample(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        ?string $offlineUserDataJobType,
        int $conversionActionId,
        ?int $externalId,
        ?string $advertiserUploadDateTime,
        ?string $bridgeMapVersionId,
        ?int $partnerId
    ) {
        $offlineUserDataJobServiceClient = $googleAdsClient->getOfflineUserDataJobServiceClient();

        // Creates an offline user data job for uploading transactions.
        $offlineUserDataJobResourceName = self::createOfflineUserDataJob(
            $offlineUserDataJobServiceClient,
            $customerId,
            $offlineUserDataJobType,
            $externalId,
            $advertiserUploadDateTime,
            $bridgeMapVersionId,
            $partnerId
        );

        // Adds transactions to the job.
        self::addTransactionsToOfflineUserDataJob(
            $offlineUserDataJobServiceClient,
            $customerId,
            $offlineUserDataJobResourceName,
            $conversionActionId
        );

        // Issues an asynchronous request to run the offline user data job.
        $offlineUserDataJobServiceClient->runOfflineUserDataJob($offlineUserDataJobResourceName);

        printf(
            "Sent request to asynchronously run offline user data job: '%s'.%s",
            $offlineUserDataJobResourceName,
            PHP_EOL
        );

        // Offline user data jobs may take up to 24 hours to complete, so instead of waiting for the
        // job to complete, retrieves and displays the job status once and then prints the query to
        // use to check the job again later.
        self::checkJobStatus($googleAdsClient, $customerId, $offlineUserDataJobResourceName);
    }

    /**
     * Creates an offline user data job for uploading store sales transactions.
     *
     * @param OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient the offline user
     *     data job service client
     * @param int $customerId the customer ID
     * @param string|null $offlineUserDataJobType the type of offline user data in the job (first
     *     party or third party). If you have an official store sales partnership with Google, use
     *     `STORE_SALES_UPLOAD_THIRD_PARTY`. Otherwise, use `STORE_SALES_UPLOAD_FIRST_PARTY`
     * @param int|null $externalId optional (but recommended) external ID for the offline user data
     *     job
     * @param string|null $advertiserUploadDateTime date and time the advertiser uploaded data to
     *     the partner. Only required for third party uploads
     * @param string|null $bridgeMapVersionId version of partner IDs to be used for uploads. Only
     *     required for third party uploads
     * @param int|null $partnerId ID of the third party partner. Only required for third party
     *     uploads
     * @return string the resource name of the created job
     */
    private static function createOfflineUserDataJob(
        OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient,
        int $customerId,
        ?string $offlineUserDataJobType,
        ?int $externalId,
        ?string $advertiserUploadDateTime,
        ?string $bridgeMapVersionId,
        ?int $partnerId
    ): string {
        // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses the
        // term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is called
        // loyaltyFraction in the Google Ads API.
        // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
        // details.
        $storeSalesMetadata = new StoreSalesMetadata([
            // Sets the fraction of your overall sales that you (or the advertiser, in the third
            // party case) can associate with a customer (email, phone number, address, etc.) in
            // your database or loyalty program.
            // For example, set this to 0.7 if you have 100 transactions over 30 days, and out of
            // those 100 transactions, you can identify 70 by an email address or phone number.
            'loyalty_fraction' => 0.7,
            // Sets the fraction of sales you're uploading out of the overall sales that you (or the
            // advertiser, in the third party case) can associate with a customer. In most cases,
            // you will set this to 1.0.
            // Continuing the example above for loyalty fraction, a value of 1.0 here indicates that
            // you are uploading all 70 of the transactions that can be identified by an email
            // address or phone number.
            'transaction_upload_fraction' => 1.0,
        ]);
        if (
            OfflineUserDataJobType::value($offlineUserDataJobType)
                === OfflineUserDataJobType::STORE_SALES_UPLOAD_THIRD_PARTY
        ) {
            // Creates additional metadata required for uploading third party data.
            $storeSalesThirdPartyMetadata = new StoreSalesThirdPartyMetadata([
                // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                'advertiser_upload_date_time' => $advertiserUploadDateTime,
                // Sets the fraction of transactions you received from the advertiser that have
                // valid formatting and values. This captures any transactions the advertiser
                // provided to you but which you are unable to upload to Google due to formatting
                // errors or missing data.
                // In most cases, you will set this to 1.0.
                'valid_transaction_fraction' => 1.0,
                // Sets the fraction of valid transactions (as defined above) you received from the
                // advertiser that you (the third party) have matched to an external user ID on your
                // side.
                // In most cases, you will set this to 1.0.
                'partner_match_fraction' => 1.0,
                // Sets the fraction of transactions you (the third party) are uploading out of the
                // transactions you received from the advertiser that meet both of the following
                // criteria:
                // 1. Are valid in terms of formatting and values. See valid transaction fraction
                // above.
                // 2. You matched to an external user ID on your side. See partner match fraction
                // above.
                // In most cases, you will set this to 1.0.
                'partner_upload_fraction' => 1.0,
                // Please speak with your Google representative to get the values to use for the
                // bridge map version and partner IDs.
                // Sets the version of partner IDs to be used for uploads.
                'bridge_map_version_id' => $bridgeMapVersionId,
                // Sets the third party partner ID uploading the transactions.
                'partner_id' => $partnerId,
            ]);
            $storeSalesMetadata->setThirdPartyMetadata($storeSalesThirdPartyMetadata);
        }
        // Creates a new offline user data job.
        $offlineUserDataJob = new OfflineUserDataJob([
            'type' => OfflineUserDataJobType::value($offlineUserDataJobType),
            'store_sales_metadata' => $storeSalesMetadata
        ]);
        if (!is_null($externalId)) {
            $offlineUserDataJob->setExternalId($externalId);
        }

        // 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
        );

        return $offlineUserDataJobResourceName;
    }

    /**
     * Adds operations to the job for a set of sample transactions.
     *
     * @param OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient the offline user
     *     data job service client
     * @param int $customerId the customer ID
     * @param string $offlineUserDataJobResourceName the resource name of the created offline user
     *     data job
     * @param int $conversionActionId the ID of a store sales conversion action
     */
    private static function addTransactionsToOfflineUserDataJob(
        OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient,
        int $customerId,
        string $offlineUserDataJobResourceName,
        int $conversionActionId
    ) {
        // Constructs the operation for each transaction.
        $userDataJobOperations =
            self::buildOfflineUserDataJobOperations($customerId, $conversionActionId);
        // Issues a request to add the operations to the offline user data job.
        /** @var AddOfflineUserDataJobOperationsResponse $operationResponse */
        $response = $offlineUserDataJobServiceClient->addOfflineUserDataJobOperations(
            $offlineUserDataJobResourceName,
            $userDataJobOperations,
            ['enablePartialFailure' => 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())) {
            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.%s",
                count($response->getPartialFailureError()->getDetails()),
                count($userDataJobOperations),
                $response->getPartialFailureError()->getMessage(),
                PHP_EOL
            );
        } else {
            printf(
                "Successfully added %d operations to the offline user data job.%s",
                count($userDataJobOperations),
                PHP_EOL
            );
        }
    }

    /**
     * Creates a list of offline user data job operations for sample transactions.
     *
     * @param int $customerId the customer ID
     * @param int $conversionActionId the ID of a store sales conversion action
     * @return OfflineUserDataJobOperation[] an array with the operations
     */
    private static function buildOfflineUserDataJobOperations(
        $customerId,
        $conversionActionId
    ): array {
        // Creates the first transaction for upload based on an email address and state.
        $userDataWithEmailAddress = new UserData([
            'user_identifiers' => [
                new UserIdentifier([
                    // Email addresses must be normalized and hashed.
                    'hashed_email' => self::normalizeAndHash('customer@example.com')
                ]),
                new UserIdentifier([
                    'address_info' => new OfflineUserAddressInfo(['state' => 'NY'])
                ])
            ],
            'transaction_attribute' => new TransactionAttribute([
                'conversion_action'
                    => ResourceNames::forConversionAction($customerId, $conversionActionId),
                'currency_code' => 'USD',
                // Converts the transaction amount from $200 USD to micros.
                'transaction_amount_micros' => 200 * 1000000,
                // Specifies the date and time of the transaction. The format is
                // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                // timezone offset from UTC. If the offset is absent, the API will
                // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                // or "2018-02-01 14:34:30+03:00".
                'transaction_date_time' => '2020-05-01 23:52:12'
            ])
        ]);

        // Creates the second transaction for upload based on a physical address.
        $userDataWithPhysicalAddress = new UserData([
            'user_identifiers' => [
                new UserIdentifier([
                    'address_info' => new OfflineUserAddressInfo([
                        // First and last name must be normalized and hashed.
                        'hashed_first_name' => self::normalizeAndHash('John'),
                        'hashed_last_name' => self::normalizeAndHash('Doe'),
                        // Country code and zip code are sent in plain text.
                        'country_code' => 'US',
                        'postal_code' => '10011'
                    ])
                ])
            ],
            'transaction_attribute' => new TransactionAttribute([
                'conversion_action'
                    => ResourceNames::forConversionAction($customerId, $conversionActionId),
                'currency_code' => 'EUR',
                // Converts the transaction amount from 450 EUR to micros.
                'transaction_amount_micros' => 450 * 1000000,
                // Specifies the date and time of the transaction. This date and time will be
                // interpreted by the API using the Google Ads customer's time zone.
                // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                'transaction_date_time' => '2020-05-14 19:07:02'
            ])
        ]);

        // Creates the operations to add the two transactions.
        $operations = [];
        foreach ([$userDataWithEmailAddress, $userDataWithPhysicalAddress] as $userData) {
            $operations[] = new OfflineUserDataJobOperation(['create' => $userData]);
        }

        return $operations;
    }

    /**
     * Returns the result of normalizing and then hashing the string.
     * Private customer data must be hashed during upload, as described at
     * https://support.google.com/google-ads/answer/7506124.
     *
     * @param string $value the value to normalize and hash
     * @return string the normalized and hashed value
     */
    private static function normalizeAndHash(string $value): string
    {
        return hash('sha256', strtolower(trim($value)));
    }

    /**
     * Retrieves, checks, and prints the status of the offline user data job.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $offlineUserDataJobResourceName the resource name of the created offline user
     *     data job
     */
    private static function checkJobStatus(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $offlineUserDataJobResourceName
    ) {
        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();

        // Creates a query that retrieves the offline user data.
        $query = "SELECT offline_user_data_job.resource_name, "
            . "offline_user_data_job.id, "
            . "offline_user_data_job.status, "
            . "offline_user_data_job.type, "
            . "offline_user_data_job.failure_reason "
            . "FROM offline_user_data_job "
            . "WHERE offline_user_data_job.resource_name = '$offlineUserDataJobResourceName'";

        // Issues a search stream request.
        /** @var GoogleAdsServerStreamDecorator $stream */
        $stream = $googleAdsServiceClient->searchStream($customerId, $query);

        // Prints out some information about the offline user data.
        /** @var GoogleAdsRow $googleAdsRow */
        $googleAdsRow = $stream->iterateAllElements()->current();
        $offlineUserDataJob = $googleAdsRow->getOfflineUserDataJob();
        printf(
            "Offline user data job ID %d with type '%s' has status: %s.%s",
            $offlineUserDataJob->getId(),
            OfflineUserDataJobType::name($offlineUserDataJob->getType()),
            OfflineUserDataJobStatus::name($offlineUserDataJob->getStatus()),
            PHP_EOL
        );

        if (OfflineUserDataJobStatus::FAILED === $offlineUserDataJob->getStatus()) {
            printf(
                "  Failure reason: %s%s",
                OfflineUserDataJobFailureReason::name($offlineUserDataJob->getFailureReason()),
                PHP_EOL
            );
        } elseif (
            OfflineUserDataJobStatus::PENDING === $offlineUserDataJob->getStatus()
            || OfflineUserDataJobStatus::RUNNING === $offlineUserDataJob->getStatus()
        ) {
            printf(
                '%1$sTo check the status of the job periodically, use the following GAQL '
                . 'query with GoogleAdsService.search:%1$s%2$s%1$s.',
                PHP_EOL,
                $query
            );
        }
    }
}

UploadStoreSalesTransactions::main();

Perl

#!/usr/bin/perl -w
#
# Copyright 2020, 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
#
#     http://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.
#
# This example uploads offline data for store sales transactions.
#
# This feature is only available to allowlisted accounts.
# See https://support.google.com/google-ads/answer/7620302 for more details.

use strict;
use warnings;
use utf8;

use FindBin qw($Bin);
use lib "$Bin/../../lib";

use Google::Ads::GoogleAds::Client;
use Google::Ads::GoogleAds::Utils::GoogleAdsHelper;
use Google::Ads::GoogleAds::V6::Resources::OfflineUserDataJob;
use Google::Ads::GoogleAds::V6::Common::OfflineUserAddressInfo;
use Google::Ads::GoogleAds::V6::Common::StoreSalesMetadata;
use Google::Ads::GoogleAds::V6::Common::StoreSalesThirdPartyMetadata;
use Google::Ads::GoogleAds::V6::Common::TransactionAttribute;
use Google::Ads::GoogleAds::V6::Common::UserData;
use Google::Ads::GoogleAds::V6::Common::UserIdentifier;
use Google::Ads::GoogleAds::V6::Enums::OfflineUserDataJobTypeEnum
  qw(STORE_SALES_UPLOAD_FIRST_PARTY STORE_SALES_UPLOAD_THIRD_PARTY);
use
  Google::Ads::GoogleAds::V6::Services::OfflineUserDataJobService::OfflineUserDataJobOperation;
use Google::Ads::GoogleAds::V6::Utils::ResourceNames;

use Getopt::Long qw(:config auto_help);
use Pod::Usage;
use Cwd qw(abs_path);
use Digest::SHA qw(sha256_hex);

use constant POLL_FREQUENCY_SECONDS => 1;
use constant POLL_TIMEOUT_SECONDS   => 60;

# The following parameter(s) should be provided to run the example. You can
# either specify these by changing the INSERT_XXX_ID_HERE values below, or on
# the command line.
#
# Parameters passed on the command line will override any parameters set in
# code.
#
# Running the example with -h will print the command line usage.
my $customer_id          = "INSERT_CUSTOMER_ID_HERE";
my $conversion_action_id = "INSERT_CONVERSION_ACTION_ID_HERE";
#
# Optional: Specify the type of user data in the job (first or third party).
# If you have an official store sales partnership with Google, use
# STORE_SALES_UPLOAD_THIRD_PARTY.
# Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.
my $offline_user_data_job_type = STORE_SALES_UPLOAD_FIRST_PARTY;
# Optional: Specify an external ID below to identify the offline user data job.
# If none is specified, this example will create an external ID.
my $external_id = undef;
# Optional: Specify an advertiser upload date time for third party data.
my $advertiser_upload_date_time = undef;
# Optional: Specify a bridge map version ID for third party data.
my $bridge_map_version_id = undef;
# Optional: Specify a partner ID for third party data.
my $partner_id = undef;

sub upload_store_sales_transactions {
  my (
    $api_client,                 $customer_id,
    $offline_user_data_job_type, $conversion_action_id,
    $external_id,                $advertiser_upload_date_time,
    $bridge_map_version_id,      $partner_id
  ) = @_;

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

  # Create an offline user data job for uploading transactions.
  my $offline_user_data_job_resource_name = create_offline_user_data_job(
    $offline_user_data_job_service, $customer_id,
    $offline_user_data_job_type,    $external_id,
    $advertiser_upload_date_time,   $bridge_map_version_id,
    $partner_id
  );

  # Add transactions to the job.
  add_transactions_to_offline_user_data_job(
    $offline_user_data_job_service,       $customer_id,
    $offline_user_data_job_resource_name, $conversion_action_id
  );

  # Issue an asynchronous request to run the offline user data job.
  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;
  }

  return 1;
}

# Create an offline user data job for uploading store sales transactions.
# Return the resource name of the created job.
sub create_offline_user_data_job {
  my (
    $offline_user_data_job_service, $customer_id,
    $offline_user_data_job_type,    $external_id,
    $advertiser_upload_date_time,   $bridge_map_version_id,
    $partner_id
  ) = @_;

  # TIP: If you are migrating from the AdWords API, please note that Google Ads
  # API uses the term "fraction" instead of "rate". For example, loyaltyRate in
  # the AdWords API is called loyaltyFraction in the Google Ads API.

  my $store_sales_metadata =
    # Please refer to https://support.google.com/google-ads/answer/7506124 for
    # additional details.
    Google::Ads::GoogleAds::V6::Common::StoreSalesMetadata->new({
      # Set the fraction of your overall sales that you (or the advertiser,
      # in the third party case) can associate with a customer (email, phone
      # number, address, etc.) in your database or loyalty program.
      # For example, set this to 0.7 if you have 100 transactions over 30
      # days, and out of those 100 transactions, you can identify 70 by an
      # email address or phone number.
      loyaltyFraction => 0.7,
      # Set the fraction of sales you're uploading out of the overall sales
      # that you (or the advertiser, in the third party case) can associate
      # with a customer. In most cases, you will set this to 1.0.
      # Continuing the example above for loyalty fraction, a value of 1.0 here
      # indicates that you are uploading all 70 of the transactions that can
      # be identified by an email address or phone number.
      transactionUploadFraction => 1.0
    });

  if ($offline_user_data_job_type eq STORE_SALES_UPLOAD_THIRD_PARTY) {
    # Create additional metadata required for uploading third party data.
    my $store_sales_third_party_metadata =
      Google::Ads::GoogleAds::V6::Common::StoreSalesThirdPartyMetadata->new({
        # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
        advertiserUploadDateTime => $advertiser_upload_date_time,

        # Set the fraction of transactions you received from the advertiser
        # that have valid formatting and values. This captures any transactions
        # the advertiser provided to you but which you are unable to upload to
        # Google due to formatting errors or missing data.
        # In most cases, you will set this to 1.0.
        validTransactionFraction => 1.0,
        # Set the fraction of valid transactions (as defined above) you
        # received from the advertiser that you (the third party) have matched
        # to an external user ID on your side.
        # In most cases, you will set this to 1.0.
        partnerMatchFraction => 1.0,

        # Set the fraction of transactions you (the third party) are uploading
        # out of the transactions you received from the advertiser that meet
        # both of the following criteria:
        # 1. Are valid in terms of formatting and values. See valid transaction
        # fraction above.
        # 2. You matched to an external user ID on your side. See partner match
        # fraction above.
        # In most cases, you will set this to 1.0.
        partnerUploadFraction => 1.0,

        # Please speak with your Google representative to get the values to use
        # for the bridge map version and partner IDs.

        # Set the version of partner IDs to be used for uploads.
        bridgeMapVersionId => $bridge_map_version_id,
        # Set the third party partner ID uploading the transactions.
        partnerId => $partner_id,
      });
    $store_sales_metadata->{thirdPartyMetadata} =
      $store_sales_third_party_metadata;
  }

  # Create a new offline user data job.
  my $offline_user_data_job =
    Google::Ads::GoogleAds::V6::Resources::OfflineUserDataJob->new({
      type               => $offline_user_data_job_type,
      storeSalesMetadata => $store_sales_metadata,
      external_id        => $external_id,
    });

  # 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;
  return $offline_user_data_job_resource_name;
}

# Add operations to the job for a set of sample transactions.
sub add_transactions_to_offline_user_data_job {
  my (
    $offline_user_data_job_service,       $customer_id,
    $offline_user_data_job_resource_name, $conversion_action_id
  ) = @_;

  # 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(
        $customer_id, $conversion_action_id
      )});

  # 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";
}

# Create a list of offline user data job operations for sample transactions.
# Return a list of operations.
sub build_offline_user_data_job_operations {
  my ($customer_id, $conversion_action_id) = @_;

  # Create the first transaction for upload based on an email address and state.
  my $user_data_with_email_address =
    Google::Ads::GoogleAds::V6::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V6::Common::UserIdentifier->new({
            # Hash normalized email addresses based on SHA-256 hashing algorithm.
            hashedEmail => normalize_and_hash('customer@example.com')}
        ),
        Google::Ads::GoogleAds::V6::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V6::Common::OfflineUserAddressInfo->new({
                state => "NY"
              })})
      ],
      transactionAttribute =>
        Google::Ads::GoogleAds::V6::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V6::Utils::ResourceNames::conversion_action(
            $customer_id, $conversion_action_id
            ),
          currencyCode => "USD",
          # Convert the transaction amount from $200 USD to micros.
          transactionAmountMicros => 200000000,
          # Specify the date and time of the transaction. The format is
          # "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional timezone
          # offset from UTC. If the offset is absent, the API will use the
          # account's timezone as default. Examples: "2018-03-05 09:15:00"
          # or "2018-02-01 14:34:30+03:00".
          transactionDateTime => "2020-05-01 23:52:12",
        })});

  # Create the second transaction for upload based on a physical address.
  my $user_data_with_physical_address =
    Google::Ads::GoogleAds::V6::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V6::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V6::Common::OfflineUserAddressInfo->new({
                # First and last name must be normalized and hashed.
                hashedFirstName => normalize_and_hash("John"),
                hashedLastName  => normalize_and_hash("Doe"),
                # Country code and zip code are sent in plain text.
                countryCode => "US",
                postalCode  => "10011"
              })})
      ],
      transactionAttribute =>
        Google::Ads::GoogleAds::V6::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V6::Utils::ResourceNames::conversion_action(
            $customer_id,
            $conversion_action_id
            ),
          currencyCode => "EUR",
          # Convert the transaction amount from 450 EUR to micros.
          transactionAmountMicros => 450000000,
          # Specify the date and time of the transaction. This date and time
          # will be interpreted by the API using the Google Ads customer's
          # time zone. The date/time must be in the format
          # "yyyy-MM-dd hh:mm:ss".
          transactionDateTime => "2020-05-14 19:07:02",
        })});

  # Create the operations to add the two transactions.
  my $operations = [
    Google::Ads::GoogleAds::V6::Services::OfflineUserDataJobService::OfflineUserDataJobOperation
      ->new({
        create => $user_data_with_email_address
      }
      ),
    Google::Ads::GoogleAds::V6::Services::OfflineUserDataJobService::OfflineUserDataJobOperation
      ->new({
        create => $user_data_with_physical_address
      })];

  return $operations;
}

# Return the result of normalizing and then hashing the string using the
# provided digest. Private customer data must be hashed during upload, as
# described at https://support.google.com/google-ads/answer/7506124
sub normalize_and_hash {
  my $value = shift;

  $value =~ s/^\s+|\s+$//g;
  return sha256_hex(lc $value);
}

# Don't run the example if the file is being included.
if (abs_path($0) ne abs_path(__FILE__)) {
  return 1;
}

# Get Google Ads Client, credentials will be read from ~/googleads.properties.
my $api_client = Google::Ads::GoogleAds::Client->new();

# By default examples are set to die on any server returned fault.
$api_client->set_die_on_faults(1);

# Parameters passed on the command line will override any parameters set in code.
GetOptions(
  "customer_id=s"                 => \$customer_id,
  "offline_user_data_job_type=s"  => \$offline_user_data_job_type,
  "conversion_action_id=i"        => \$conversion_action_id,
  "external_id=i"                 => \$external_id,
  "advertiser_upload_date_time=s" => \$advertiser_upload_date_time,
  "bridge_map_version_id=i"       => \$bridge_map_version_id,
  "partner_id=i"                  => \$partner_id,
);

# Print the help message if the parameters are not initialized in the code nor
# in the command line.
pod2usage(2)
  if not check_params($customer_id, $conversion_action_id);

# Call the example.
upload_store_sales_transactions(
  $api_client,                 $customer_id =~ s/-//gr,
  $offline_user_data_job_type, $conversion_action_id,
  $external_id,                $advertiser_upload_date_time,
  $bridge_map_version_id,      $partner_id,
);

=pod

=head1 NAME

upload_store_sales_transactions

=head1 DESCRIPTION

This example uploads offline data for store sales transactions.

This feature is only available to allowlisted accounts.
See https://support.google.com/google-ads/answer/7620302 for more details.

=head1 SYNOPSIS

upload_store_sales_transactions.pl [options]

    -help                           Show the help message.
    -customer_id                    The Google Ads customer ID.
    -conversion_action_id           The ID of a store sales conversion action.
    -offline_user_data_job_type     [optional] The type of offline user data in the job (first party or third party).
                                    If you have an official store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY.
                                    Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY.
    -external_id                    [optional] (but recommended) external ID for the offline user data job.
    -advertiser_upload_date_time    [optional] Date and time the advertiser uploaded data to the partner. Only required for third party uploads.
                                    The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. "2019-01-01 12:32:45-08:00".
    -bridge_map_version_id          [optional] Version of partner IDs to be used for uploads. Only required for third party uploads.
    -partner_id                     [optional] ID of the third party partner. Only required for third party uploads.

=cut

Uploading OfflineUserDataJob

There are several requirements that must be met when adding a UserData object to an OfflineUserDataJob resource.

To avoid an OfflineUserDataJobError.INVALID_CONVERSION_ACTION error, the conversion_action attribute must refer to a ConversionAction where:

  • The ConversionAction had a status of ENABLED at the time of the impression.

  • The ConversionAction existed in the effective conversion account of the click's Google Ads account at the time of the impression. If the account was not using cross-account conversion tracking at the time of the impression, Google Ads will look for the ConversionAction in the account used to upload conversions. You can find your account's effective conversion tracking account under TOOLS & SETTINGS > Conversions in the Google Ads UI, or the conversion_tracking_setting attribute of Customer in the Google Ads API, but keep in mind that this will show you the current effective conversion tracking account, which may differ from the effective conversion tracking account in place at the time of the impression.

In addition, the following conditions must be met:

  • The transaction_amount_micros must be greater than zero.

  • The transaction_date_time must have a timezone specified, and the format is as yyyy-mm-dd hh:mm:ss+|-hh:mm, e.g., 2019-01-01 12:32:45-08:00. The timezone can be for any valid value: it does not have to match the account's timezone.