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.

Read through the store sales Help Center articles and follow the checklist to better understand what is needed based on if you're a retailer, restaurant, or other type of business.

Set up your Google Ads account for store sales

Check your eligibility, work with your Google representative to get your account set up on an allowlist, and create your conversion actions by following Set up your Google Ads account for store sales.

You can only create store sales conversion actions through the Google Ads UI. When retrieving these conversion actions through the Google Ads API, you will see one of the following:

Both types can be based on first-party and third-party merchant data uploads. STORE_SALES can include in-store purchases using cards from payment networks.

Create custom variables (only for retail and restaurant businesses)

You can create custom variables for your store sales conversion action and upload your offline transactions to predefined custom variable values. This feature enables you to upload your offline sales data at a more granular level, and to see your online-to-offline conversions and ROI at slices that matter to your business.

You have to create a store sales custom variable and its custom variable values using the Google Ads UI before uploading. Each transaction should be uploaded with at most one custom variable value, otherwise reporting inaccuracies will be caused.

When uploading store sales data with a custom variable using the Google Ads API, you need to specify the custom variable as the custom_key attribute in StoreSalesMetadata, and provide the custom variable value as the custom_value in the TransactionAttribute.

Custom variable API errors that you might encounter are CUSTOM_KEY_DISABLED, CUSTOM_KEY_NOT_PREDEFINED, CUSTOM_KEY_NOT_SET.

Upload your store sales data

After creating the conversion action, you are ready to start uploading store sales conversions using either the UI or the Google Ads API. The steps are:

  • Determine if you are third-party or first-party. If you have an official store sales partnership with Google, then you are third-party. Otherwise, you are first-party.
  • Create an OfflineUserDataJob. If you are third-party, set the type of the job to STORE_SALES_UPLOAD_THIRD_PARTY. Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY. Only fill in StoreSalesMetadata.third_party_metadata if this is a third-party job.
  • Add store sales transactions to the job. You need to associate the store sales conversions with the conversion action 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.
  • Run the asynchronous job.
  • Offline user data jobs may take up to 24 hours to complete. You can periodically check the job's status through GoogleAdsService.SearchStream.

Include shopping items with transactions

In addition to transaction attributes such as the date and amount, you can also provide information about the items in each transaction by adding item_attributes to the transaction_attribute. Each ItemAttribute has the following fields:

item_id
The offer ID of the item in your Merchant Center feed, or the item's GTIN (Global Trade Item Number). Populate this field if merchant_id, region_code, or language_code is specified.
merchant_id
The ID of the Merchant Center account of the item's feed.
country_code
The Common Locale Data Repository (CLDR) territory code of the item's Merchant Center feed.
language_code
The ISO 639-1 language code of the item's Merchant Center feed.
quantity
The number of items sold. Defaults to 1 if not set.
If setting this value, you must also set at least one of item_id, merchant_id, country_code, or language_code.

Tips

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:

To avoid the job failing with OfflineUserDataJobFailureReason.INSUFFICIENT_MATCHED_TRANSACTIONS, there must be at least 50 matched transactions in a job. Upload at least a few hundred transactions so that at least 50 match customers. Only 50 transactions can be uploaded at one time, so multiple uploads to the same job are required.

In addition, the following conditions must be met:

  • At the time of the click, conversion tracking was enabled in the effective effective conversion account of the click's Google Ads account.
  • The transaction_amount_micros must be greater than zero.
  • The transaction_date_time can have an optional timezone specified, and the format is as yyyy-mm-dd hh:mm:ss+|-hh:mm, for example, 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. It should be the timezone where the transaction happened. When the timezone is not provided, the account's timezone will be used.
  • Data such as emails, phone numbers, and names must be normalized and hashed using SHA-256.

Code example

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.v8.common.OfflineUserAddressInfo;
import com.google.ads.googleads.v8.common.StoreSalesMetadata;
import com.google.ads.googleads.v8.common.StoreSalesThirdPartyMetadata;
import com.google.ads.googleads.v8.common.TransactionAttribute;
import com.google.ads.googleads.v8.common.UserData;
import com.google.ads.googleads.v8.common.UserIdentifier;
import com.google.ads.googleads.v8.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus;
import com.google.ads.googleads.v8.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType;
import com.google.ads.googleads.v8.errors.GoogleAdsError;
import com.google.ads.googleads.v8.errors.GoogleAdsException;
import com.google.ads.googleads.v8.resources.OfflineUserDataJob;
import com.google.ads.googleads.v8.services.AddOfflineUserDataJobOperationsRequest;
import com.google.ads.googleads.v8.services.AddOfflineUserDataJobOperationsResponse;
import com.google.ads.googleads.v8.services.CreateOfflineUserDataJobResponse;
import com.google.ads.googleads.v8.services.GoogleAdsRow;
import com.google.ads.googleads.v8.services.GoogleAdsServiceClient;
import com.google.ads.googleads.v8.services.OfflineUserDataJobOperation;
import com.google.ads.googleads.v8.services.OfflineUserDataJobServiceClient;
import com.google.ads.googleads.v8.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;

    @Parameter(
        names = ArgumentNames.ITEM_ID,
        description =
            "Specify a unique identifier of a product, either the Merchant Center Item ID or"
                + " Global Trade Item Number (GTIN). Only required if uploading with item"
                + " attributes.")
    private String itemId;

    @Parameter(
        names = ArgumentNames.MERCHANT_CENTER_ACCOUNT_ID,
        description =
            "A Merchant Center Account ID. Only required if uploading with item attributes.")
    private Long merchantCenterAccountId;

    @Parameter(
        names = ArgumentNames.COUNTRY_CODE,
        description =
            "A two-letter country code of the location associated with the feed where your items"
                + " are uploaded. Only required if uploading with item attributes. For a list of"
                + " country codes see the country codes here:"
                + " https://developers.google.com/google-ads/api/reference/data/codes-formats#country-codes")
    private String countryCode;

    @Parameter(
        names = ArgumentNames.LANGUAGE_CODE,
        description =
            "A two-letter language code of the language associated with the feed where your items"
                + " are uploaded. Only required if uploading with item attributes. For a list of"
                + " language codes see:"
                + " https://developers.google.com/google-ads/api/reference/data/codes-formats#languages")
    private String languageCode;

    @Parameter(
        names = ArgumentNames.QUANTITY,
        description =
            "The number of items sold. Can only be set when at least one other item attribute has"
                + " been provided. Only required if uploading with item attributes.")
    private int quantity;
  }

  /** Specifies the value to use if uploading data with custom key and values. */
  private static final String CUSTOM_VALUE = null;

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

      // OPTIONAL: Specify a unique identifier of a product, either the Merchant Center
      // Item ID or Global Trade Item Number (GTIN). Only required if uploading with
      // item attributes.
      // params.itemId = Long.parseLong("INSERT_ITEM_ID_HERE");

      // OPTIONAL: Specify a Merchant Center Account ID. Only required if uploading
      // with item attributes.
      // params.merchantCenterAccountId = Long.parseLong("INSERT_MERCHANT_CENTER_ID_HERE");

      // OPTIONAL: Specify a two-letter country code of the location associated with the
      // feed where your items are uploaded. Only required if uploading with item
      // attributes.
      // params.countryCode = "INSERT_COUNTRY_CODE_HERE";

      // OPTIONAL: Specify a two-letter language code of the language associated with
      // the feed where your items are uploaded. Only required if uploading with item
      // attributes.
      // params.languageCode = "INSERT_LANGUAGE_CODE_HERE";

      // OPTIONAL: Specify a number of items sold. Only required if uploading with item
      // attributes.
      // params.quantity = 1;
    }

    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,
              params.itemId,
              params.merchantCenterAccountId,
              params.countryCode,
              params.languageCode,
              params.quantity);
    } 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.
   * @param itemId the ID of the item in merchant center (optional).
   * @param merchantCenterAccountId the ID of the merchant center account (optional).
   * @param countryCode the country code of the item for sale in merchant center.
   * @param languageCode the language of the item for sale in merchant center.
   * @param quantity the number of items that we sold.
   * @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,
      String itemId,
      Long merchantCenterAccountId,
      String countryCode,
      String languageCode,
      int quantity)
      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,
          customKey,
          itemId,
          merchantCenterAccountId,
          countryCode,
          languageCode,
          quantity);

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

      // BEWARE! The above call returns an OperationFuture. The execution of that future depends on
      // the thread pool which is owned by offlineUserDataJobServiceClient. If you use this future,
      // you *must* keep the service client in scope too.
      // See https://developers.google.com/google-ads/api/docs/client-libs/java/lro for more detail.

      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,
      String customKey,
      String itemId,
      Long merchantId,
      String countryCode,
      String languageCode,
      Integer quantity)
      throws InterruptedException, ExecutionException, TimeoutException,
          UnsupportedEncodingException {
    // Constructs the operation for each transaction.
    List<OfflineUserDataJobOperation> userDataJobOperations =
        buildOfflineUserDataJobOperations(
            customerId,
            conversionActionId,
            customKey,
            itemId,
            merchantId,
            countryCode,
            languageCode,
            quantity);

    // 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,
      String customKey,
      String itemId,
      Long merchantId,
      String countryCode,
      String languageCode,
      Integer quantity)
      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.Builder 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 assign the custom value.
    if (customKey != null) {
      userDataWithEmailAddress.getTransactionAttributeBuilder().setCustomValue(CUSTOM_VALUE);
    }

    // Creates the second transaction for upload based on a physical address.
    UserData.Builder 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"));

    if (itemId != null) {
      userDataWithPhysicalAddress
          .getTransactionAttributeBuilder()
          .getItemAttributeBuilder()
          .setItemId(itemId)
          .setMerchantId(merchantId)
          .setCountryCode(countryCode)
          .setLanguageCode(languageCode)
          .setQuantity(quantity);
    }

    // Creates the operations to add the two transactions.
    List<OfflineUserDataJobOperation> operations = new ArrayList<>();
    for (UserData userData :
        Arrays.asList(userDataWithEmailAddress.build(), userDataWithPhysicalAddress.build())) {
      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 CommandLine;
using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.V8.Common;
using Google.Ads.GoogleAds.V8.Errors;
using Google.Ads.GoogleAds.V8.Resources;
using Google.Ads.GoogleAds.V8.Services;
using static Google.Ads.GoogleAds.V8.Enums.OfflineUserDataJobStatusEnum.Types;
using static Google.Ads.GoogleAds.V8.Enums.OfflineUserDataJobTypeEnum.Types;

namespace Google.Ads.GoogleAds.Examples.V8
{
    /// <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
    {
        /// <summary>
        /// Command line options for running the <see cref="UploadStoreSalesTransactions"/> example.
        /// </summary>
        public class Options : OptionsBase
        {
            /// <summary>
            /// The Google Ads customer ID for which the call is made.
            /// </summary>
            [Option("customerId", Required = true, HelpText =
                "The Google Ads customer ID for which the call is made.")]
            public long CustomerId { get; set; }

            /// <summary>
            /// The ID of a store sales conversion action.
            /// </summary>
            [Option("conversionActionId", Required = true, HelpText =
                "The ID of a store sales conversion action.")]
            public long ConversionActionId { get; set; }

            /// <summary>
            /// 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.
            /// </summary>
            [Option("offlineUserDataJobType", Required = false, HelpText =
                    "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.",
                Default = OfflineUserDataJobType.StoreSalesUploadFirstParty)]
            public OfflineUserDataJobType OfflineUserDataJobType { get; set; }

            /// <summary>
            /// Optional (but recommended) external ID to identify the offline user data job.
            /// </summary>
            [Option("externalId", Required = false, HelpText =
                    "Optional (but recommended) external ID to identify the offline user data job.",
                Default = null)]
            public long? ExternalId { get; set; }

            /// <summary>
            /// Date and time the advertiser uploaded data to the partner. Only required if
            /// uploading third party data.
            /// </summary>
            [Option("advertiserUploadDateTime", Required = false, HelpText =
                "Date and time the advertiser uploaded data to the partner. Only required if " +
                "uploading third party data.", Default = null)]
            public string AdvertiserUploadDateTime { get; set; }

            /// <summary>
            /// Version of partner IDs to be used for uploads. Only required if uploading third
            /// party data.
            /// </summary>
            [Option("bridgeMapVersionId", Required = false, HelpText =
                "Version of partner IDs to be used for uploads. Only required if uploading " +
                "third party data.", Default = null)]
            public string BridgeMapVersionId { get; set; }

            /// <summary>
            /// ID of the third party partner. Only required if uploading third party data.
            /// </summary>
            [Option("partnerId", Required = false, HelpText =
                    "ID of the third party partner. Only required if uploading third party data.",
                Default = null)]
            public long? PartnerId { get; set; }

            /// <summary>
            /// Optional custom key name. Only required if uploading data with custom key and
            /// values.
            /// </summary>
            [Option("customKey", Required = false, HelpText =
                "Optional custom key name. Only required if uploading data with custom key and" +
                " values.", Default = null)]
            public string CustomKey { get; set; }

            /// <summary>
            /// A unique identifier of a product, either the Merchant Center Item ID or Global Trade
            /// Item Number (GTIN). Only required if uploading with item attributes.
            /// </summary>
            [Option("itemId", Required = false, HelpText =
                    "A unique identifier of a product, either the Merchant Center Item ID or " +
                    "Global Trade Item Number (GTIN). Only required if uploading with item " +
                    "attributes.",
                Default = null)]
            public string ItemId { get; set; }

            /// <summary>
            /// A Merchant Center Account ID. Only required if uploading with item attributes.
            /// </summary>
            [Option("merchantCenterAccountId", Required = false, HelpText =
                    "A Merchant Center Account ID. Only required if uploading with item " +
                    "attributes.",
                Default = null)]
            public long? MerchantCenterAccountId { get; set; }

            /// <summary>
            /// A two-letter country code of the location associated with the feed where your items
            /// are uploaded. Only required if uploading with item attributes.
            /// For a list of country codes see:
            /// https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16
            /// </summary>
            [Option("countryCode", Required = false, HelpText =
                    "A two-letter country code of the location associated with the feed where your " +
                    "items are uploaded. Only required if uploading with item attributes.\nFor a " +
                    "list of country codes see: " +
                    "https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16",
                Default = null)]
            public string CountryCode { get; set; }

            /// <summary>
            /// A two-letter language code of the language associated with the feed where your items
            /// are uploaded. Only required if uploading with item attributes. For a list of
            /// language codes see:
            /// https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7
            /// </summary>
            [Option("languageCode", Required = false, HelpText =
                    "A two-letter language code of the language associated with the feed where " +
                    "your items are uploaded. Only required if uploading with item attributes.\n" +
                    "For a list of language codes see: " +
                    "https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7",
                Default = null)]
            public string LanguageCode { get; set; }

            /// <summary>
            /// The number of items sold. Can only be set when at least one other item attribute has
            /// been provided. Only required if uploading with item attributes.
            /// </summary>
            [Option("quantity", Required = false, HelpText =
                    "The number of items sold. Only required if uploading with item attributes.",
                Default = 1)]
            public long Quantity { get; set; }
        }

        /// <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)
        {
            Options options = new Options();
            Parser.Default.ParseArguments<Options>(args).MapResult(
                delegate(Options o)
                {
                    options = o;
                    return 0;
                }, delegate(IEnumerable<Error> errors)
                {
                    // The Google Ads customer ID for which the call is made.
                    options.CustomerId = long.Parse("INSERT_CUSTOMER_ID_HERE");

                    // The ID of a store sales conversion action.
                    options.ConversionActionId = long.Parse("INSERT_CONVERSION_ACTION_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.
                    options.OfflineUserDataJobType =
                        OfflineUserDataJobType.StoreSalesUploadFirstParty;

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

                    // Date and time the advertiser uploaded data to the partner. Only required
                    // if uploading third party data.
                    options.AdvertiserUploadDateTime = "INSERT_ADVERTISER_UPLOAD_DATE_TIME_HERE";

                    // Version of partner IDs to be used for uploads. Only required if uploading
                    // third party data.
                    options.BridgeMapVersionId = "INSERT_BRIDGE_MAP_VERSION_ID_HERE";

                    // ID of the third party partner. Only required if uploading third party data.
                    options.PartnerId = long.Parse("INSERT_PARTNER_ID_HERE");

                    // Optional custom key name. Only required if uploading data with custom key
                    // and values.
                    options.CustomKey = "INSERT_CUSTOM_KEY_HERE";

                    // A unique identifier of a product, either the Merchant Center Item ID or Global Trade
                    // Item Number (GTIN). Only required if uploading with item attributes.
                    options.ItemId = "INSERT_ITEM_ID_HERE";

                    // A Merchant Center Account ID. Only required if uploading with item
                    // attributes.
                    options.MerchantCenterAccountId =
                        long.Parse("INSERT_MERCHANT_CENTER_ACCOUNT_ID_HERE");

                    // A two-letter country code of the location associated with the feed where your
                    // items are uploaded. Only required if uploading with item attributes.
                    options.CountryCode = "INSERT_COUNTRY_CODE_HERE";

                    // A two-letter language code of the language associated with the feed where
                    // your items are uploaded. Only required if uploading with item attributes.
                    options.LanguageCode = "INSERT_LANGUAGE_CODE_HERE";

                    // The number of items sold. Only required if uploading with item attributes.
                    options.Quantity = long.Parse("INSERT_QUANTITY_HERE");

                    return 0;
                });

            UploadStoreSalesTransactions codeExample = new UploadStoreSalesTransactions();
            Console.WriteLine(codeExample.Description);
            codeExample.Run(new GoogleAdsClient(), options.CustomerId, options.ConversionActionId,
                options.OfflineUserDataJobType, options.ExternalId,
                options.AdvertiserUploadDateTime, options.BridgeMapVersionId, options.PartnerId,
                options.CustomKey, options.ItemId, options.MerchantCenterAccountId,
                options.CountryCode,
                options.LanguageCode, options.Quantity);
        }

        // 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>
        /// 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>
        /// <param name="itemId">A unique identifier of a product, either the Merchant Center Item
        /// ID or Global Trade Item Number (GTIN). Only required if uploading with item
        /// attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID. Only required if uploading with
        /// item attributes.</param>
        /// <param name="countryCode">A two-letter country code of the location associated with the
        /// feed where your items are uploaded. Only required if uploading with item
        /// attributes.</param>
        /// <param name="languageCode">A two-letter language code of the language associated with
        /// the feed where your items are uploaded. Only required if uploading with item
        /// attributes.</param>
        /// <param name="quantity">The number of items sold. Only required if uploading with item
        /// attributes.</param>
        public void Run(GoogleAdsClient client, long customerId, long conversionActionId,
            OfflineUserDataJobType offlineUserDataJobType, long? externalId,
            string advertiserUploadDateTime, string bridgeMapVersionId, long? partnerId,
            string customKey, string itemId, long? merchantCenterAccountId, string countryCode,
            string languageCode, long quantity)
        {
            // Get the OfflineUserDataJobServiceClient.
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
                client.GetService(Services.V8.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, itemId,
                    merchantCenterAccountId, countryCode, languageCode, quantity);

                // 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>
        /// <param name="itemId">A unique identifier of a product, or null if not uploading with
        /// item attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID, or null if not
        /// uploading with item attributes.</param>
        /// <param name="countryCode">A two-letter country code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="languageCode">A two-letter language code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="quantity">The number of items sold, or null if not uploading with
        /// item attributes.</param>
        private void AddTransactionsToOfflineUserDataJob(
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId,
            string offlineUserDataJobResourceName, long conversionActionId, string customKey,
            string itemId, long? merchantCenterAccountId, string countryCode, string languageCode,
            long quantity)
        {
            // Constructions an operation for each transaction.
            List<OfflineUserDataJobOperation> userDataJobOperations =
                BuildOfflineUserDataJobOperations(customerId, conversionActionId, customKey, itemId,
                    merchantCenterAccountId, countryCode, languageCode, quantity);

            // 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>
        /// <param name="itemId">A unique identifier of a product, or null if not uploading with
        /// item attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID, or null if not
        /// uploading with item attributes.</param>
        /// <param name="countryCode">A two-letter country code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="languageCode">A two-letter language code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="quantity">The number of items sold, or null if not uploading with
        /// item attributes.</param>
        /// <returns>A list of operations.</returns>
        private List<OfflineUserDataJobOperation> BuildOfflineUserDataJobOperations(long customerId,
            long conversionActionId, string customKey, string itemId, long? merchantCenterAccountId,
            string countryCode, string languageCode, long quantity)
        {
            // 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.
                    // If item attributes are provided, this value represents the total value of the
                    // items after multiplying the unit price per item by the quantity provided in
                    // the ItemAttribute.
                    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")
                }
            };

            // Set the item attribute if provided.
            if (!string.IsNullOrEmpty(itemId))
            {
                userDataWithPhysicalAddress.TransactionAttribute.ItemAttribute = new ItemAttribute
                {
                    ItemId = itemId,
                    MerchantId = merchantCenterAccountId.Value,
                    CountryCode = countryCode,
                    LanguageCode = languageCode,
                    // Quantity field should only be set when at least one of the other item
                    // attributes is present.
                    Quantity = quantity
                };
            }

            // 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.V8.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\Examples\Utils\Helper;
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
use Google\Ads\GoogleAds\Lib\V8\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V8\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\V8\GoogleAdsException;
use Google\Ads\GoogleAds\Lib\V8\GoogleAdsServerStreamDecorator;
use Google\Ads\GoogleAds\Util\V8\ResourceNames;
use Google\Ads\GoogleAds\V8\Common\ItemAttribute;
use Google\Ads\GoogleAds\V8\Common\OfflineUserAddressInfo;
use Google\Ads\GoogleAds\V8\Common\StoreSalesMetadata;
use Google\Ads\GoogleAds\V8\Common\StoreSalesThirdPartyMetadata;
use Google\Ads\GoogleAds\V8\Common\TransactionAttribute;
use Google\Ads\GoogleAds\V8\Common\UserData;
use Google\Ads\GoogleAds\V8\Common\UserIdentifier;
use Google\Ads\GoogleAds\V8\Enums\OfflineUserDataJobFailureReasonEnum\OfflineUserDataJobFailureReason;
use Google\Ads\GoogleAds\V8\Enums\OfflineUserDataJobStatusEnum\OfflineUserDataJobStatus;
use Google\Ads\GoogleAds\V8\Enums\OfflineUserDataJobTypeEnum\OfflineUserDataJobType;
use Google\Ads\GoogleAds\V8\Errors\GoogleAdsError;
use Google\Ads\GoogleAds\V8\Resources\OfflineUserDataJob;
use Google\Ads\GoogleAds\V8\Services\AddOfflineUserDataJobOperationsResponse;
use Google\Ads\GoogleAds\V8\Services\CreateOfflineUserDataJobResponse;
use Google\Ads\GoogleAds\V8\Services\GoogleAdsRow;
use Google\Ads\GoogleAds\V8\Services\OfflineUserDataJobOperation;
use Google\Ads\GoogleAds\V8\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;
    /**
     * 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 const CUSTOM_KEY = 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;

    // Optional: Below constants are only required if uploading with item attributes.
    /**
     * Specify a unique identifier of a product, either the Merchant Center
     * Item ID or Global Trade Item Number (GTIN).
     */
    private const ITEM_ID = null;
    /**
     * Specify a Merchant Center Account ID.
     */
    private const MERCHANT_CENTER_ACCOUNT_ID = null;
    /**
     * Specify a two-letter country code of the location associated with the
     * feed where your items are uploaded.
     * For a list of country codes see:
     * https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16
     */
    private const COUNTRY_CODE = null;
    /**
     * Specify a two-letter language code of the language associated with
     * the feed where your items are uploaded.
     * For a list of language codes see:
     * https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7
     */
    private const LANGUAGE_CODE = null;
    /**
     * Specify a number of items sold.
     */
    private const QUANTITY = 1;

    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::CUSTOM_KEY => 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,
            ArgumentNames::ITEM_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::MERCHANT_CENTER_ACCOUNT_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::COUNTRY_CODE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::LANGUAGE_CODE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::QUANTITY => 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::CUSTOM_KEY] ?: self::CUSTOM_KEY,
                $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,
                $options[ArgumentNames::ITEM_ID] ?: self::ITEM_ID,
                $options[ArgumentNames::MERCHANT_CENTER_ACCOUNT_ID]
                    ?: self::MERCHANT_CENTER_ACCOUNT_ID,
                $options[ArgumentNames::COUNTRY_CODE] ?: self::COUNTRY_CODE,
                $options[ArgumentNames::LANGUAGE_CODE] ?: self::LANGUAGE_CODE,
                $options[ArgumentNames::QUANTITY] ?: self::QUANTITY
            );
        } 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 $customKey the custom key to segment store sales conversions. Only
     *     required after creating a custom key and custom values in the account.
     * @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
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    public static function runExample(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        ?string $offlineUserDataJobType,
        int $conversionActionId,
        ?int $externalId,
        ?string $customKey,
        ?string $advertiserUploadDateTime,
        ?string $bridgeMapVersionId,
        ?int $partnerId,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ) {
        $offlineUserDataJobServiceClient = $googleAdsClient->getOfflineUserDataJobServiceClient();

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

        // Adds transactions to the job.
        self::addTransactionsToOfflineUserDataJob(
            $offlineUserDataJobServiceClient,
            $customerId,
            $offlineUserDataJobResourceName,
            $conversionActionId,
            $itemId,
            $merchantCenterAccountId,
            $countryCode,
            $languageCode,
            $quantity
        );

        // 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 $customKey the custom key to segment store sales conversions. Only
     *     required after creating a custom key and custom values in the account.
     * @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 $customKey,
        ?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 (!is_null($customKey)) {
            $storeSalesMetadata->setCustomKey($customKey);
        }
        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
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    private static function addTransactionsToOfflineUserDataJob(
        OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient,
        int $customerId,
        string $offlineUserDataJobResourceName,
        int $conversionActionId,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ) {
        // Constructs the operation for each transaction.
        $userDataJobOperations = self::buildOfflineUserDataJobOperations(
            $customerId,
            $conversionActionId,
            $itemId,
            $merchantCenterAccountId,
            $countryCode,
            $languageCode,
            $quantity
        );
        // 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
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    private static function buildOfflineUserDataJobOperations(
        $customerId,
        $conversionActionId,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ): 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' => Helper::baseToMicro(200),
                // 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'
                // OPTIONAL: If uploading data with custom key and values, also specify the
                // following value:
                // 'custom_value' => 'INSERT_CUSTOM_VALUE_HERE'
            ])
        ]);

        // 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' => Helper::baseToMicro(450),
                // 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'
            ])
        ]);

        // Optional: If uploading data with item attributes, also assign these values
        // in the transaction attribute.
        if (!empty($itemId)) {
            $userDataWithPhysicalAddress->getTransactionAttribute()->setItemAttribute(
                new ItemAttribute([
                    'item_id' => $itemId,
                    'merchant_id' => $merchantCenterAccountId,
                    'country_code' => $countryCode,
                    'language_code' => $languageCode,
                    // Quantity field should only be set when at least one of the other item
                    // attribute fields is present.
                    'quantity' => $quantity
                ])
            );
        }

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

      

Python

#!/usr/bin/env python
# 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.
"""This example uploads offline conversion 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.
"""

import argparse
from datetime import datetime
import hashlib
import sys

from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException


def main(
    client,
    customer_id,
    conversion_action_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
):
    """Uploads offline conversion data for store sales transactions.

    Args:
        client: An initialized Google Ads client.
        customer_id: The Google Ads customer ID.
        conversion_action_id: The ID of a store sales conversion action.
        offline_user_data_job_type: Optional 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.
        custom_key: A custom key str to segment store sales conversions. Only
            required after creating a custom key and custom values in the
            account.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.
    """
    # Get the OfflineUserDataJobService and OperationService clients.
    offline_user_data_job_service = client.get_service(
        "OfflineUserDataJobService"
    )

    # Create an offline user data job for uploading transactions.
    offline_user_data_job_resource_name = _create_offline_user_data_job(
        client,
        offline_user_data_job_service,
        customer_id,
        offline_user_data_job_type,
        external_id,
        advertiser_upload_date_time,
        bridge_map_version_id,
        partner_id,
        custom_key,
    )

    # Add transactions to the job.
    _add_transactions_to_offline_user_data_job(
        client,
        offline_user_data_job_service,
        customer_id,
        offline_user_data_job_resource_name,
        conversion_action_id,
        custom_value,
    )

    # Issue an asynchronous request to run the offline user data job.
    offline_user_data_job_service.run_offline_user_data_job(
        resource_name=offline_user_data_job_resource_name
    )

    # 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.
    _check_job_status(client, customer_id, offline_user_data_job_resource_name)


def _create_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
):
    """Creates an offline user data job for uploading store sales transactions.

    Args:
        client: An initialized Google Ads API client.
        offline_user_data_job_service: The offline user data job service client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_type: Optional type of offline user data in the
            job (first party or third party).
        external_id: Optional 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.
        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.
        custom_key: A custom key str to segment store sales conversions. Only
            required after creating a custom key and custom values in the
            account.

    Returns:
        The string resource name of the created job.
    """
    # TIP: If you are migrating from the AdWords API, please note that Google
    # Ads API uses the term "fraction" instead of "rate". For example,
    # loyalty_rate in the AdWords API is called loyalty_fraction in the Google
    # Ads API.

    # Create a new offline user data job.
    offline_user_data_job = client.get_type("OfflineUserDataJob")
    offline_user_data_job.type_ = offline_user_data_job_type
    if external_id is not None:
        offline_user_data_job.external_id = external_id

    # Please refer to https://support.google.com/google-ads/answer/7506124 for
    # additional details.
    store_sales_metadata = offline_user_data_job.store_sales_metadata
    # 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.
    store_sales_metadata.loyalty_fraction = 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.
    store_sales_metadata.transaction_upload_fraction = 1.0

    if custom_key:
        store_sales_metadata.custom_key = custom_key

    if (
        offline_user_data_job_type
        == client.enums.OfflineUserDataJobTypeEnum.STORE_SALES_UPLOAD_THIRD_PARTY
    ):
        # Create additional metadata required for uploading third party data.
        store_sales_third_party_metadata = (
            store_sales_metadata.third_party_metadata
        )
        # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
        store_sales_third_party_metadata.advertiser_upload_date_time = (
            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.
        store_sales_third_party_metadata.valid_transaction_fraction = 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.
        store_sales_third_party_metadata.partner_match_fraction = 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.
        store_sales_third_party_metadata.partner_upload_fraction = 1.0
        # Set 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.
        store_sales_third_party_metadata.bridge_map_version_id = (
            bridge_map_version_id
        )
        # Set the third party partner ID uploading the transactions.
        store_sales_third_party_metadata.partner_id = partner_id

    create_offline_user_data_job_response = (
        offline_user_data_job_service.create_offline_user_data_job(
            customer_id=customer_id, job=offline_user_data_job
        )
    )
    offline_user_data_job_resource_name = (
        create_offline_user_data_job_response.resource_name
    )
    print(
        "Created an offline user data job with resource name "
        f"'{offline_user_data_job_resource_name}'."
    )
    return offline_user_data_job_resource_name


def _add_transactions_to_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_resource_name,
    conversion_action_id,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
):
    """Add operations to the job for a set of sample transactions.

    Args:
        client: An initialized Google Ads API client.
        offline_user_data_job_service: The offline user data job service client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_resource_name: The string resource name of the
            offline user data job that will receive the transactions.
        conversion_action_id: The ID of a store sales conversion action.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.
    """
    # Construct some sample transactions.
    operations = _build_offline_user_data_job_operations(
        client,
        customer_id,
        conversion_action_id,
        item_id,
        merchant_center_account_id,
        country_code,
        language_code,
        quantity,
    )

    # Issue a request to add the operations to the offline user data job.
    request = client.get_type("AddOfflineUserDataJobOperationsRequest")
    request.resource_name = offline_user_data_job_resource_name
    request.enable_partial_failure = True
    request.operations = operations
    response = (
        offline_user_data_job_service.add_offline_user_data_job_operations(
            request=request,
        )
    )

    # 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.py to learn more.
    num_partial_failures = len(response.partial_failure_error.details)
    if response.partial_failure_error:
        print(
            f"{num_partial_failures} partial failure error(s) occurred: "
            f"{response.partial_failure_error.message}."
        )

    print(
        f"{len(operations) - num_partial_failures} operations were "
        "successfully added to the offline user data job."
    )


def _build_offline_user_data_job_operations(
    client,
    customer_id,
    conversion_action_id,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
):
    """Create offline user data job operations for sample transactions.

    Args:
        client: An initialized Google Ads API client.
        customer_id: The Google Ads customer ID.
        conversion_action_id: The ID of a store sales conversion action.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.

    Returns:
        A list of OfflineUserDataJobOperations.
    """
    # Create the first transaction for upload with an email address and state.
    user_data_with_email_address_operation = client.get_type(
        "OfflineUserDataJobOperation"
    )
    user_data_with_email_address = user_data_with_email_address_operation.create
    email_identifier = client.get_type("UserIdentifier")
    # Hash normalized email addresses based on SHA-256 hashing algorithm.
    email_identifier.hashed_email = _normalize_and_hash("customer@example.com")
    state_identifier = client.get_type("UserIdentifier")
    state_identifier.address_info.state = "NY"
    user_data_with_email_address.user_identifiers.extend(
        [email_identifier, state_identifier]
    )
    user_data_with_email_address.transaction_attribute.conversion_action = (
        client.get_service("ConversionActionService").conversion_action_path(
            customer_id, conversion_action_id
        )
    )
    user_data_with_email_address.transaction_attribute.currency_code = "USD"
    # Convert the transaction amount from $200 USD to micros.
    user_data_with_email_address.transaction_attribute.transaction_amount_micros = (
        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".
    user_data_with_email_address.transaction_attribute.transaction_date_time = (
        datetime.now() - datetime.timedelta(months=1)
    ).strftime("%Y-%m-%d %H:%M:%S")

    # Create the second transaction for upload based on a physical address.
    user_data_with_physical_address_operation = client.get_type(
        "OfflineUserDataJobOperation"
    )
    user_data_with_physical_address = (
        user_data_with_physical_address_operation.create
    )
    address_identifier = client.get_type("UserIdentifier")
    # First and last name must be normalized and hashed.
    address_identifier.address_info.hashed_first_name = _normalize_and_hash(
        "John"
    )
    address_identifier.address_info.hashed_last_name = _normalize_and_hash(
        "Doe"
    )
    # Country and zip codes are sent in plain text.
    address_identifier.address_info.country_code = "US"
    address_identifier.address_info.postal_code = "10011"
    user_data_with_physical_address.user_identifiers.append(address_identifier)
    user_data_with_physical_address.transaction_attribute.conversion_action = (
        client.get_service("ConversionActionService").conversion_action_path(
            customer_id, conversion_action_id
        )
    )
    user_data_with_physical_address.transaction_attribute.currency_code = "EUR"
    # Convert the transaction amount from 450 EUR to micros.
    user_data_with_physical_address.transaction_attribute.transaction_amount_micros = (
        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".
    user_data_with_physical_address.transaction_attribute.transaction_date_time = (
        datetime.now() - datetime.timedelta(days=1)
    ).strftime(
        "%Y-%m-%d %H:%M:%S"
    )

    if custom_value:
        user_data_with_email_address.transaction_attribute.custom_value = (
            custom_value
        )
        user_data_with_physical_address.transaction_attribute.custom_value = (
            custom_value
        )

    # Optional: If uploading data with item attributes, also assign these
    # values in the transaction attribute
    if item_id:
        item_attribute = (
            user_data_with_physical_address.transaction_attribute.item_attribute
        )
        item_attribute.item_id = item_id
        item_attribute.merchant_id = merchant_center_account_id
        item_attribute.country_code = country_code
        item_attribute.language_code = language_code
        item_attribute.quantity = quantity

    return [
        user_data_with_email_address_operation,
        user_data_with_physical_address_operation,
    ]


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

    Args:
        s: The string to perform this operation on.

    Returns:
        A normalized (lowercase, remove whitespace) and SHA-256 hashed string.
    """
    return hashlib.sha256(s.strip().lower().encode()).hexdigest()


def _check_job_status(client, customer_id, offline_user_data_job_resource_name):
    """Retrieves, checks, and prints the status of the offline user data job.

    Args:
        client: An initialized Google Ads API client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_resource_name: The resource name of the job whose
            status you wish to check.
    """
    # Get the GoogleAdsService client.
    googleads_service = client.get_service("GoogleAdsService")

    # Construct a query to fetch the job status.
    query = f"""
        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 =
          '{offline_user_data_job_resource_name}'"""

    # Issue the query and get the GoogleAdsRow containing the job.
    googleads_row = next(
        iter(googleads_service.search(customer_id=customer_id, query=query))
    )
    offline_user_data_job = googleads_row.offline_user_data_job

    offline_user_data_job_type_enum = (
        client.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType
    )
    offline_user_data_job_status_enum = (
        client.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus
    )

    job_status = offline_user_data_job.status
    print(
        f"Offline user data job ID {offline_user_data_job.id} with type "
        f"'{offline_user_data_job_type_enum.Name(offline_user_data_job.type)}' "
        f"has status {offline_user_data_job_status_enum.Name(job_status)}."
    )

    offline_user_data_job_status_enum = (
        client.enums.OfflineUserDataJobStatusEnum
    )
    if job_status == offline_user_data_job_status_enum.FAILED:
        print(f"\tFailure reason: {offline_user_data_job.failure_reason}")
    elif (
        job_status == offline_user_data_job_status_enum.PENDING
        or job_status == offline_user_data_job_status_enum.RUNNING
    ):
        print(
            "\nTo check the status of the job periodically, use the "
            f"following GAQL query with GoogleAdsService.Search:\n{query}\n"
        )
    elif job_status == offline_user_data_job_status_enum.SUCCESS:
        print("\nThe requested job has completed successfully.")
    else:
        raise ValueError("Requested job has UNKNOWN or UNSPECIFIED status.")


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

    parser = argparse.ArgumentParser(
        description="This example uploads offline data for store sales "
        "transactions."
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID.",
    )
    parser.add_argument(
        "-a",
        "--conversion_action_id",
        type=int,
        required=True,
        help="The ID of a store sales conversion action.",
    )
    parser.add_argument(
        "-k",
        "--custom_key",
        type=str,
        required=False,
        help="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. If provided, a custom value must also be provided",
    )
    parser.add_argument(
        "-v",
        "--custom_value",
        type=str,
        required=False,
        help="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. If provided, a custom key must also be provided",
    )
    parser.add_argument(
        "-o",
        "--offline_user_data_job_type",
        type=int,
        required=False,
        default=googleads_client.enums.OfflineUserDataJobTypeEnum.STORE_SALES_UPLOAD_FIRST_PARTY,
        help="Optional 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, defaults to "
        "STORE_SALES_UPLOAD_FIRST_PARTY.",
    )
    parser.add_argument(
        "-e",
        "--external_id",
        type=int,
        required=False,
        default=None,
        help="Optional, but recommended, external ID for the offline user data "
        "job.",
    )
    parser.add_argument(
        "-d",
        "--advertiser_upload_date_time",
        type=str,
        required=False,
        default=None,
        help="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. '2021-01-01 12:32:45-08:00'.",
    )
    parser.add_argument(
        "-b",
        "--bridge_map_version_id",
        type=str,
        required=False,
        default=None,
        help="Optional version of partner IDs to be used for uploads. Only "
        "required for third party uploads.",
    )
    parser.add_argument(
        "-p",
        "--partner_id",
        type=int,
        required=False,
        default=None,
        help="Optional ID of the third party partner. Only required for third "
        "party uploads.",
    )
    parser.add_argument(
        "-i",
        "--item_id",
        type=str,
        required=False,
        default=None,
        help="Optional ID of the product. Either the Merchant Center Item ID "
        "or the Global Trade Item Number (GTIN). Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-m",
        "--merchant_center_account_id",
        type=int,
        required=False,
        default=None,
        help="Optional Merchant Center Account ID. Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-r",
        "--country_code",
        type=str,
        required=False,
        default=None,
        help="Optional two-letter country code of the location associated with "
        "the feed where your items are uploaded. Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-l",
        "--language_code",
        type=str,
        required=False,
        default=None,
        help="Optional two-letter language code of the language associated "
        "with the feed where your items are uploaded. Only required if "
        "uploading with item attributes.",
    )
    parser.add_argument(
        "-q",
        "--quantity",
        type=int,
        required=False,
        default=1,
        help="Optional number of items sold. Only required if uploading with "
        "item attributes.",
    )
    args = parser.parse_args()

    # Additional check to make sure that custom_key and custom_value are either
    # not provided or both provided together.
    required_together = ("custom_key", "custom_value")
    required_custom_vals = [
        getattr(args, field, None) for field in required_together
    ]
    if any(required_custom_vals) and not all(required_custom_vals):
        parser.error(
            "--custom_key (-k) and --custom_value (-v) must be passed "
            "in together"
        )

    try:
        main(
            googleads_client,
            args.customer_id,
            args.conversion_action_id,
            args.offline_user_data_job_type,
            args.external_id,
            args.advertiser_upload_date_time,
            args.bridge_map_version_id,
            args.partner_id,
            args.custom_key,
            args.custom_value,
            args.item_id,
            args.merchant_center_account_id,
            args.country_code,
            args.language_code,
            args.quantity,
        )
    except GoogleAdsException as ex:
        print(
            f"Request with ID '{ex.request_id}' failed with status "
            f"'{ex.error.code().name}' and includes the following errors:"
        )
        for error in ex.failure.errors:
            print(f"\tError with message '{error.message}'.")
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print(f"\t\tOn field: {field_path_element.field_name}")
        sys.exit(1)

      

Ruby

#!/usr/bin/env ruby
# Encoding: utf-8
#
# 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.
#
# 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.

require 'date'
require 'digest'
require 'google/ads/google_ads'
require 'optparse'

def upload_store_sales_transactions(
  customer_id,
  offline_user_data_job_type,
  conversion_action_id,
  external_id,
  advertiser_upload_date_time,
  bridge_map_version_id,
  partner_id,
  custom_key,
  custom_value,
  item_id,
  merchant_center_account_id,
  region_code,
  language_code,
  quantity)
  # GoogleAdsClient will read a config file from
  # ENV['HOME']/google_ads_config.rb when called without parameters
  client = Google::Ads::GoogleAds::GoogleAdsClient.new
  offline_user_data_job_service = client.service.offline_user_data_job

  # Creates an offline user data job for uploading transactions.
  offline_user_data_job_resource_name = create_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
  )

  # Add transactions to the job
  add_transactions_to_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_resource_name,
    conversion_action_id,
    custom_value,
  )

  # Issues an asynchronous request to run the offline user data job.
  offline_user_data_job_service.run_offline_user_data_job(
    resource_name: offline_user_data_job_resource_name,
  )

  puts "Sent request to asynchronously run offline user data job: " \
    "#{offline_user_data_job_resource_name}"

  # 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.
  check_job_status(client, customer_id, offline_user_data_job_resource_name)
end

# Creates an offline user data job for uploading store sales transactions.
def create_offline_user_data_job(
  client,
  offline_user_data_job_service,
  customer_id,
  offline_user_data_job_type,
  external_id,
  advertiser_upload_date_time,
  bridge_map_version_id,
  partner_id,
  custom_key)
  # TIP: If you are migrating from the AdWords API, please note tha Google Ads
  # API uses the term "fraction" instead of "rate". For example, loyalty_rate
  # in the AdWords API is called loyalty_fraction in the Google Ads API.
  # Please refer to https://support.google.com/google-ads/answer/7506124 for
  # additional details.
  store_sales_metadata = client.resource.store_sales_metadata do |s|
    # 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.
    s.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.
    s.transaction_upload_fraction = 1.0
    s.custom_key = custom_key unless custom_key.nil?
  end

  # Creates additional metadata required for uploading third party data.
  if offline_user_data_job_type == :STORE_SALES_UPLOAD_THIRD_PARTY
    store_sales_metadata.third_party_metadata =
      client.resource.store_sales_third_party_metadata do |t|
      # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
      t.advertiser_upload_date_time = advertiser_upload_date_time
      # 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.
      t.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.
      t.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.
      t.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.
      t.bridge_map_version_id = bridge_map_version_id
      # Sets the third party partner ID uploading the transactions.
      t.partner_id = partner_id.to_i
    end
  end

  # Creates a new offline user data job.
  offline_user_data_job = client.resource.offline_user_data_job do |job|
    job.type = offline_user_data_job_type
    job.store_sales_metadata = store_sales_metadata
  end

  unless external_id.nil?
    offline_user_data_job.external_id = external_id.to_i
  end

  # Issues a request to create the offline user data job.
  response = offline_user_data_job_service.create_offline_user_data_job(
    customer_id: customer_id,
    job: offline_user_data_job,
  )

  offline_user_data_job_resource_name = response.resource_name
  puts "Created an offline user data job with resource name: " \
    "#{offline_user_data_job_resource_name}."

  offline_user_data_job_resource_name
end

# Adds operations to the job for a set of sample transactions.
def add_transactions_to_offline_user_data_job(
  client,
  offline_user_data_job_service,
  customer_id,
  offline_user_data_job_resource_name,
  conversion_action_id,
  custom_value)
  # Constructs the operation for each transaction.
  user_data_job_operations = build_offline_user_data_job_operations(
    client, customer_id, conversion_action_id, custom_value)

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

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

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

# Creates a list of offline user data job operations for sample transactions.
def build_offline_user_data_job_operations(
  client,
  customer_id,
  conversion_action_id,
  custom_value)
  operations = []

  # Creates the first transaction for upload based on an email address
  # and state.
  operations << client.operation.create_resource.offline_user_data_job do |job|
    job.user_identifiers << client.resource.user_identifier do |id|
      # Email addresses must be normalized and hashed.
      id.hashed_email = normalize_and_hash("customer@example.com")
    end
    job.user_identifiers << client.resource.user_identifier do |id|
      id.address_info = client.resource.offline_user_address_info do |info|
        info.state = "NY"
      end
    end
    job.transaction_attribute = client.resource.transaction_attribute do |t|
      t.conversion_action = client.path.conversion_action(
        customer_id, conversion_action_id)
      t.currency_code = "USD"
      # Converts the transaction amount from $200 USD to micros.
      t.transaction_amount_micros = 200_000_000
      # 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".
      t.transaction_date_time = "2020-05-01 23:52:12"
      t.custom_value = custom_value unless custom_value.nil?
    end
  end

  # Creates the second transaction for upload based on a physical address.
  operations << client.operation.create_resource.offline_user_data_job do |job|
    job.user_identifiers << client.resource.user_identifier do |id|
      id.address_info = client.resource.offline_user_address_info do |info|
        # First and last name must be normalized and hashed.
        info.hashed_first_name = normalize_and_hash("John")
        info.hashed_last_name = normalize_and_hash("Doe")
        # Country code and zip code are sent in plain text.
        info.country_code = "US"
        info.postal_code = "10011"
      end
    end
    job.transaction_attribute = client.resource.transaction_attribute do |t|
      t.conversion_action = client.path.conversion_action(
        customer_id, conversion_action_id)
      t.currency_code = "EUR"
      # Converts the transaction amount from 450 EUR to micros.
      t.transaction_amount_micros = 450_000_000
      # 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".
      t.transaction_date_time = "2020-05-14 19:07:02"
      t.custom_value = custom_value unless custom_value.nil?
      if item_id
        t.item_attribute = client.resource.item_attribute do |item|
          item.item_id = item_id
          item.merchant_id = merchant_center_account_id.to_i
          item.region_code = region_code
          item.language_code = language_code
          item.quantity = quantity.to_i
        end
      end
    end
  end

  # Returns the operations containing the two transactions.
  operations
end

# 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.
def normalize_and_hash(str)
  Digest::SHA256.hexdigest(str.strip.downcase)
end

# Retrieves, checks, and prints the status of the offline user data job.
def check_job_status(
  client,
  customer_id,
  offline_user_data_job_resource_name)
  # Creates a query that retrieves the offline user data.
  query = <<~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 = "#{offline_user_data_job_resource_name}"
  QUERY

  puts query

  # Issues a search stream request.
  responses = client.service.google_ads.search_stream(
    customer_id: customer_id,
    query: query,
  )

  # Prints out some information about the offline user data.
  offline_user_data_job = responses.first.results.first.offline_user_data_job
  puts "Offline user data job ID #{offline_user_data_job.id} " \
    "with type #{offline_user_data_job.type} " \
    "has status: #{offline_user_data_job.status}"

  if offline_user_data_job.status == :FAILED
    puts "  Failure reason: #{offline_user_data_job.failure_reason}"
  elsif offline_user_data_job.status == :PENDING \
      || offline_user_data_job.status == :RUNNING
    puts "To check the status of the job periodically, use the following GAQL " \
      "query with google_ads.search:"
    puts query
  end
end

if __FILE__ == $0
  options = {}
  # 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.
  options[:customer_id] = 'INSERT_CUSTOMER_ID_HERE'
  options[:conversion_action_id] = 'INSERT_CONVERSION_ACTION_ID_HERE'
  options[:offline_user_data_job_type] = "STORE_SALES_UPLOAD_FIRST_PARTY"

  OptionParser.new do |opts|
    opts.banner = sprintf('Usage: %s [options]', File.basename(__FILE__))

    opts.separator ''
    opts.separator 'Options:'

    opts.on('-C', '--customer-id CUSTOMER-ID', String, 'Customer ID') do |v|
      options[:customer_id] = v
    end

    opts.on('-c', '--conversion-action-id CONVERSION-ACTION-ID', String,
      'The ID of a store sales conversion action') do |v|
      options[:conversion_action_id] = v
    end

    opts.on('-T', '--offline-user-data-job-type OFFLINE-USER-DATA-JOB-TYPE', String,
      '(Optional) 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.') do |v|
      options[:offline_user_data_job_type] = v
    end

    opts.on('-E', '--external-id EXTERNAL-ID', String,
      '(Optional, but recommended) external ID to identify the offline ' \
      'user data job') do |v|
      options[:external_id] = v
    end

    opts.on('-U', '--advertiser-upload-date-time ADVERTISER-UPLOAD-DATE-TIME', String,
      '(Only required if uploading third party data) Specify the date and time ' \
      'the advertiser uploaded data to the partner. ' \
      'The format is "yyyy-mm-dd hh:mm:ss"') do |v|
      options[:advertiser_upload_date_time] = v
    end

    opts.on('-B', '--bridge-map-version-id BRIDGE-MAP-VERSION-ID', String,
      '(Only required if uploading third party data) ' \
      'The version of partner IDs to be used for uploads.') do |v|
      options[:bridge_map_version_id] = v
    end

    opts.on('-P', '--partner-id PARTNER-ID', String,
      '(Only required if uploading third party data) ' \
      'The ID of the third party partner. ') do |v|
      options[:partner_id] = v
    end

    opts.on('-k' '--custom-key CUSTOM-KEY', String,
      '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. If provided, a custom value must also be provided') do |v|
      options[:custom_key] = v
    end

    opts.on('-v' '--custom-value CUSTOM-VALUE', String,
      '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. If provided, a custom key must also be provided') do |v|
      options[:custom_value] = v
    end

    opts.on('-i', '--item-id ITEM-ID', String,
      'Optional: Specify a unique identifier of a product, either the ' \
      'Merchant Center Item ID or Global Trade Item Number (GTIN). ' \
      'Only required if uploading with item attributes.') do |v|
      options[:item_id] = v
    end

    opts.on('-m', '--merchant-center-account-id MERCHANT-CENTER-ACCOUNT-ID', String,
      'Optional: Specify a Merchant Center Account ID. Only required if ' \
      'uploading with item attributes.') do |v|
      options[:merchant_center_account_id] = v
    end

    opts.on('-r', '--region-code REGION-CODE', String,
      'Optional: Specify a two-letter region code of the location associated ' \
      'with the feed where your items are uploaded. Only required if ' \
      'uploading with item attributes.') do |v|
      options[:region_code] = v
    end

    opts.on('-L', '--language-code LANGUAGE-CODE', String,
      'Optional: Specify a two-letter language code of the language ' \
      'associated with the feed where your items are uploaded. Only required ' \
      'if uploading with item attributes.') do |v|
      options[:language_code] = v
    end

    opts.on('-q', '--quantity QUANTITY', String,
      'Optional: Specify a number of items sold. Only required if uploading ' \
      'with item attributes.') do |v|
      options[:quantity] = v
    end

    opts.separator ''
    opts.separator 'Help:'

    opts.on_tail('-h', '--help', 'Show this message') do
      puts opts
      exit
    end
  end.parse!

  begin
    upload_store_sales_transactions(
      options.fetch(:customer_id).tr("-", ""),
      options[:offline_user_data_job_type].to_sym,
      options.fetch(:conversion_action_id),
      options[:external_id],
      options[:advertiser_upload_date_time],
      options[:bridge_map_version_id],
      options[:partner_id],
      options[:custom_key],
      options[:custom_value],
      options[:item_id],
      options[:merchant_center_account_id],
      options[:region_code],
      options[:language_code],
      options[:quantity],
    )
  rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e
    e.failure.errors.each do |error|
      STDERR.printf("Error with message: %s\n", error.message)
      if error.location
        error.location.field_path_elements.each do |field_path_element|
          STDERR.printf("\tOn field: %s\n", field_path_element.field_name)
        end
      end
      error.error_code.to_h.each do |k, v|
        next if v == :UNSPECIFIED
        STDERR.printf("\tType: %s\n\tCode: %s\n", k, v)
      end
    end
  end
end

      

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::V8::Resources::OfflineUserDataJob;
use Google::Ads::GoogleAds::V8::Common::ItemAttribute;
use Google::Ads::GoogleAds::V8::Common::OfflineUserAddressInfo;
use Google::Ads::GoogleAds::V8::Common::StoreSalesMetadata;
use Google::Ads::GoogleAds::V8::Common::StoreSalesThirdPartyMetadata;
use Google::Ads::GoogleAds::V8::Common::TransactionAttribute;
use Google::Ads::GoogleAds::V8::Common::UserData;
use Google::Ads::GoogleAds::V8::Common::UserIdentifier;
use Google::Ads::GoogleAds::V8::Enums::OfflineUserDataJobTypeEnum
  qw(STORE_SALES_UPLOAD_FIRST_PARTY STORE_SALES_UPLOAD_THIRD_PARTY);
use
  Google::Ads::GoogleAds::V8::Services::OfflineUserDataJobService::OfflineUserDataJobOperation;
use Google::Ads::GoogleAds::V8::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;
# If uploading data with custom key and values, specify the value.
use constant CUSTOM_VALUE => "INSERT_CUSTOM_VALUE_HERE";

# 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 the custom key if uploading data with custom key and values.
my $custom_key = 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;
# Optional: Specify a unique identifier of a product, either the Merchant Center
# Item ID or Global Trade Item Number (GTIN). Only required if uploading with
# item attributes.
my $item_id = undef;
# Optional: Specify a Merchant Center Account ID. Only required if uploading
# with item attributes.
my $merchant_center_account_id = undef;
# Optional: Specify a two-letter region code of the location associated with the
# feed where your items are uploaded. Only required if uploading with item
# attributes.
my $region_code = undef;
# Optional: Specify a two-letter language code of the language associated with
# the feed where your items are uploaded. Only required if uploading with item
# attributes.
my $language_code = undef;
# Optional: Specify a number of items sold. Only required if uploading with item
# attributes.
my $quantity = 1;

sub upload_store_sales_transactions {
  my (
    $api_client,                  $customer_id,
    $offline_user_data_job_type,  $conversion_action_id,
    $external_id,                 $custom_key,
    $advertiser_upload_date_time, $bridge_map_version_id,
    $partner_id,                  $item_id,
    $merchant_center_account_id,  $region_code,
    $language_code,               $quantity
  ) = @_;

  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,
    $custom_key,                    $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,
    $custom_key,                          $item_id,
    $merchant_center_account_id,          $region_code,
    $language_code,                       $quantity,
  );

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

# Creates an offline user data job for uploading store sales transactions.
# Returns 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,
    $custom_key,                    $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::V8::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
    });

  # Apply the custom key if provided.
  $store_sales_metadata->{customKey} = $custom_key if defined $custom_key;

  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::V8::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::V8::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;
}

# Adds 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,
    $custom_key,                          $item_id,
    $merchant_center_account_id,          $region_code,
    $language_code,                       $quantity
  ) = @_;

  # 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,
        $custom_key,                 $item_id,
        $merchant_center_account_id, $region_code,
        $language_code,              $quantity,
      )});

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

# Creates a list of offline user data job operations for sample transactions.
# Returns a list of operations.
sub build_offline_user_data_job_operations {
  my ($customer_id, $conversion_action_id, $custom_key, $item_id,
    $merchant_center_account_id, $region_code, $language_code, $quantity)
    = @_;

  # Create the first transaction for upload based on an email address and state.
  my $user_data_with_email_address =
    Google::Ads::GoogleAds::V8::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V8::Common::UserIdentifier->new({
            # Hash normalized email addresses based on SHA-256 hashing algorithm.
            hashedEmail => normalize_and_hash('customer@example.com')}
        ),
        Google::Ads::GoogleAds::V8::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V8::Common::OfflineUserAddressInfo->new({
                state => "NY"
              })})
      ],
      transactionAttribute =>
        Google::Ads::GoogleAds::V8::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V8::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",
        })});

  # Optional: If uploading data with custom key and values, also assign the
  # custom value.
  if (defined($custom_key)) {
    $user_data_with_email_address->{transactionAttribute}{customValue} =
      CUSTOM_VALUE;
  }

  # Create the second transaction for upload based on a physical address.
  my $user_data_with_physical_address =
    Google::Ads::GoogleAds::V8::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V8::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V8::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::V8::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V8::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",
        })});

  # Optional: If uploading data with item attributes, also assign these values
  # in the transaction attribute.
  if (defined($item_id)) {
    $user_data_with_physical_address->{transactionAttribute}{itemAttribute} =
      Google::Ads::GoogleAds::V8::Common::ItemAttribute->new({
        itemId       => $item_id,
        merchantId   => $merchant_center_account_id,
        regionCode   => $region_code,
        languageCode => $language_code,
        # Quantity field should only be set when at least one of the other item
        # attributes is present.
        quantity     => $quantity
      });

  }

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

  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
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,
  "custom_key=s"                  => \$custom_key,
  "advertiser_upload_date_time=s" => \$advertiser_upload_date_time,
  "bridge_map_version_id=i"       => \$bridge_map_version_id,
  "partner_id=i"                  => \$partner_id,
  "item_id=s"                     => \$item_id,
  "merchant_center_account_id=i"  => \$merchant_center_account_id,
  "region_code=s"                 => \$region_code,
  "language_code=s"               => \$language_code,
  "quantity=i"                    => \$quantity,
);

# 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,                 $custom_key,
  $advertiser_upload_date_time, $bridge_map_version_id,
  $partner_id,                  $item_id,
  $merchant_center_account_id,  $region_code,
  $language_code,               $quantity,
);

=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.
    -custom_key                     [optional] 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.
    -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.
    -item_id                        [optional] A unique identifier of a product, either the Merchant Center Item ID or Global Trade Item Number (GTIN).
                                    Only required if uploading with item attributes.
    -merchant_center_account_id     [optional] A Merchant Center Account ID. Only required if uploading with item attributes.
    -region_code                    [optional] A two-letter region code of the location associated with the feed where your items are uploaded.
                                    Only required if uploading with item attributes.
                                    For a list of region codes see: https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16
    -language_code                  [optional] A two-letter language code of the language associated with the feed where your items are uploaded.
                                    Only required if uploading with item attributes.
                                    For a list of language codes see: https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7
    -quantity                       [optional] The number of items sold. Can only be set when at least one other item attribute has been provided. Only required if uploading with item attributes.

=cut