The Google Ads API supports uploading store sales conversions that lets you import offline transactions into Google Ads either directly or through third-party partners. By matching transaction data from your point-of-sale system 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 to use store sales.
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 web
interface. When retrieving
the conversion actions through the Google Ads API, you get a
ConversionAction
with the
type
attribute as
STORE_SALES
.
The store sales conversion action type 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
You can create custom variables for your store sales conversion action and upload your offline transactions to predefined custom variable values. This feature lets you 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 web interface before uploading. Each transaction should be uploaded with at most one custom variable value, otherwise, reporting inaccuracies can occur.
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 include the following:
Upload your store sales data
After creating the conversion action, you are ready to start uploading store sales conversions using either the web interface or the API. Here are the steps:
- You must set the
customer_id
field in your OfflineUserDataJobService requests to the customer ID of the Google Ads conversion customer of your Google Ads account. - 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 toSTORE_SALES_UPLOAD_THIRD_PARTY
. Otherwise, useSTORE_SALES_UPLOAD_FIRST_PARTY
. Only fill inStoreSalesMetadata.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 createdOfflineUserDataJob
resource through theOfflineUserDataJobService
. Ensure that you populate theconsent
field of theUserData
object. Advertisers must pass store sales data with consent values for both ad user data and ad personalisation for European Economic Area (EEA) users to maintain access to Customer Match functionalities. Only data with granted values will be usable for Customer Match with store sales. Review the FAQ for additional information. - Run the asynchronous job by calling
RunOfflineUserDataJob
. The job'sstatus
changes fromPENDING
toRUNNING
when the job starts. - Offline user data jobs can take up to 24 hours to complete. You can
periodically check the job's status through
GoogleAdsService.SearchStream
. The job has completed when itsstatus
changes to eitherSUCCESS
orFAILED
.
Requirements
There are several details and 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
with the following conditions:
- The
status
of theConversionAction
isENABLED
. - The
type
isSTORE_SALES
. - The
ConversionAction
exists in the Google Ads conversion customer of the click's Google Ads account.
When uploading store sales transactions, the following limits apply:
- The
operations
collection for eachAddOfflineUserDataJobOperationsRequest
can contain at most 100,000 identifiers across all of theUserData
objects in the operations. - There are no limits on the number of operations you can add to a single job.
However, for optimal processing, we recommend adding up to 10,000 operations
in a single call to the
OfflineUserDataJobService.AddOfflineUserDataJobOperations
method and up to 1,000,000 operations to a single job. - Upload jobs with insufficient transactions might fail with
OfflineUserDataJobFailureReason.INSUFFICIENT_MATCHED_TRANSACTIONS
. Upload at least a few hundred transactions to increase the likelihood that your job reaches relevant thresholds.
In addition, the following conditions must be met:
- At the time of the click, conversion tracking was enabled in the Google Ads conversion customer 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 asyyyy-mm-dd HH:mm:ss+|-HH:mm
, for example:2022-01-01 19:32:45-05:00
(ignoring daylight saving time) . 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 is 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.v17.common.Consent; import com.google.ads.googleads.v17.common.OfflineUserAddressInfo; import com.google.ads.googleads.v17.common.StoreSalesMetadata; import com.google.ads.googleads.v17.common.StoreSalesThirdPartyMetadata; import com.google.ads.googleads.v17.common.TransactionAttribute; import com.google.ads.googleads.v17.common.UserData; import com.google.ads.googleads.v17.common.UserIdentifier; import com.google.ads.googleads.v17.enums.ConsentStatusEnum.ConsentStatus; import com.google.ads.googleads.v17.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus; import com.google.ads.googleads.v17.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType; import com.google.ads.googleads.v17.errors.GoogleAdsError; import com.google.ads.googleads.v17.errors.GoogleAdsException; import com.google.ads.googleads.v17.errors.GoogleAdsFailure; import com.google.ads.googleads.v17.resources.OfflineUserDataJob; import com.google.ads.googleads.v17.services.AddOfflineUserDataJobOperationsRequest; import com.google.ads.googleads.v17.services.AddOfflineUserDataJobOperationsResponse; import com.google.ads.googleads.v17.services.CreateOfflineUserDataJobResponse; import com.google.ads.googleads.v17.services.GoogleAdsRow; import com.google.ads.googleads.v17.services.GoogleAdsServiceClient; import com.google.ads.googleads.v17.services.OfflineUserDataJobOperation; import com.google.ads.googleads.v17.services.OfflineUserDataJobServiceClient; import com.google.ads.googleads.v17.utils.ErrorUtils; import com.google.ads.googleads.v17.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; @Parameter(names = ArgumentNames.AD_PERSONALIZATION_CONSENT, required = false) private ConsentStatus adPersonalizationConsent; @Parameter(names = ArgumentNames.AD_USER_DATA_CONSENT, required = false) private ConsentStatus adUserDataConsent; } /** 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: specify the ad user data consent. // params.adUserDataConsent = ConsentStatus.valueOf("INSERT_AD_USER_DATA_CONSENT_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.adPersonalizationConsent, params.adUserDataConsent, 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 adPersonalizationConsent the ad personalization consent status. * @param adUserDataConsent the ad user data consent status. * @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, ConsentStatus adPersonalizationConsent, ConsentStatus adUserDataConsent, 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, adPersonalizationConsent, adUserDataConsent, 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, ConsentStatus adPersonalizationConsent, ConsentStatus adUserDataConsent, 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, adPersonalizationConsent, adUserDataConsent, 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) // Enables warnings (optional). .setEnableWarnings(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()) { GoogleAdsFailure googleAdsFailure = ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError()); googleAdsFailure .getErrorsList() .forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage())); 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", ErrorUtils.getInstance().getFailedOperationIndices(googleAdsFailure).size(), userDataJobOperations.size(), response.getPartialFailureError().getMessage()); // Checks if any warnings occurred and displays details. if (response.hasWarning()) { // Converts the Any in response back to a GoogleAdsFailure object. GoogleAdsFailure warningsFailure = ErrorUtils.getInstance().getGoogleAdsFailure(response.getWarning()); // Prints some information about the warnings encountered. System.out.println( System.out.printf("Encountered %d warning(s).%n", warningsFailure.getErrorsCount())); } } 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, ConsentStatus adPersonalizationConsent, ConsentStatus adUserDataConsent, 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, "dana@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")); // Adds consent information if specified. if (adPersonalizationConsent != null || adUserDataConsent != null) { Consent.Builder consentBuilder = Consent.newBuilder(); if (adPersonalizationConsent != null) { consentBuilder.setAdPersonalization(adPersonalizationConsent); } if (adUserDataConsent != null) { consentBuilder.setAdUserData(adUserDataConsent); } // Specifies whether user consent was obtained for the data you are uploading. See // https://www.google.com/about/company/user-consent-policy for details. userDataWithEmailAddress.setConsent(consentBuilder); } // 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, "Dana")) .setHashedLastName(normalizeAndHash(sha256Digest, "Quinn")) .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.Gax.Examples; using Google.Ads.GoogleAds.Lib; using Google.Ads.GoogleAds.V17.Common; using Google.Ads.GoogleAds.V17.Errors; using Google.Ads.GoogleAds.V17.Resources; using Google.Ads.GoogleAds.V17.Services; using static Google.Ads.GoogleAds.V17.Enums.ConsentStatusEnum.Types; using static Google.Ads.GoogleAds.V17.Enums.OfflineUserDataJobStatusEnum.Types; using static Google.Ads.GoogleAds.V17.Enums.OfflineUserDataJobTypeEnum.Types; namespace Google.Ads.GoogleAds.Examples.V17 { /// <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> /// The consent status for ad personalization. /// </summary> [Option("adPersonalizationConsent", Required = false, HelpText = "The consent status for ad user data.")] public ConsentStatus? AdPersonalizationConsent { get; set; } /// <summary> /// The consent status for ad user data. /// </summary> [Option("adUserDataConsent", Required = false, HelpText = "The consent status for ad user data.")] public ConsentStatus? AdUserDataConsent { 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 = ExampleUtilities.ParseCommandLine<Options>(args); 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, options.AdPersonalizationConsent, options.AdUserDataConsent); } // 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> /// <param name="adPersonalizationConsent">The consent status for ad personalization. /// </param> /// <param name="adUserDataConsent">The consent status for ad user data.</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, ConsentStatus? adPersonalizationConsent, ConsentStatus? adUserDataConsent) { // Get the OfflineUserDataJobServiceClient. OfflineUserDataJobServiceClient offlineUserDataJobServiceClient = client.GetService(Services.V17.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, adPersonalizationConsent, adUserDataConsent); // 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> /// <param name="adPersonalizationConsent">The consent status for ad personalization. /// </param> /// <param name="adUserDataConsent">The consent status for ad user data.</param> private void AddTransactionsToOfflineUserDataJob( OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId, string offlineUserDataJobResourceName, long conversionActionId, string customKey, string itemId, long? merchantCenterAccountId, string countryCode, string languageCode, long quantity, ConsentStatus? adPersonalizationConsent, ConsentStatus? adUserDataConsent) { // Constructions an operation for each transaction. List<OfflineUserDataJobOperation> userDataJobOperations = BuildOfflineUserDataJobOperations(customerId, conversionActionId, customKey, itemId, merchantCenterAccountId, countryCode, languageCode, quantity, adPersonalizationConsent, adUserDataConsent); // Constructs a request with partial failure enabled to add the operations to the // offline user data job, and enable_warnings set to true to retrieve warnings. AddOfflineUserDataJobOperationsRequest request = new AddOfflineUserDataJobOperationsRequest() { EnablePartialFailure = true, ResourceName = offlineUserDataJobResourceName, Operations = { userDataJobOperations }, EnableWarnings = true, }; AddOfflineUserDataJobOperationsResponse response = offlineUserDataJobServiceClient .AddOfflineUserDataJobOperations(request); // Prints the status message if any partial failure error is returned. // NOTE: The details of each partial failure error are not printed here, you can refer // to the example HandlePartialFailure.cs to learn more. if (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."); } // Prints the number of warnings if any warnings are returned. You can access // details of each warning using the same approach you'd use for partial failure // errors. if (request.EnableWarnings && response.Warnings != null) { // Extracts the warnings from the response. GoogleAdsFailure warnings = response.Warnings; Console.WriteLine($"{warnings.Errors.Count} warning(s) occurred"); } } /// <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> /// <param name="adPersonalizationConsent">The consent status for ad personalization. /// </param> /// <param name="adUserDataConsent">The consent status for ad user data.</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, ConsentStatus? adPersonalizationConsent, ConsentStatus? adUserDataConsent) { // 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("dana@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; } if (adUserDataConsent != null || adPersonalizationConsent != null) { // Specifies whether user consent was obtained for the data you are uploading. See // https://www.google.com/about/company/user-consent-policy // for details. userDataWithEmailAddress.Consent = new Consent(); if (adPersonalizationConsent != null) { userDataWithEmailAddress.Consent.AdPersonalization = (ConsentStatus)adPersonalizationConsent; } if (adUserDataConsent != null) { userDataWithEmailAddress.Consent.AdUserData = (ConsentStatus)adUserDataConsent; } } // 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("Alex"), HashedLastName = NormalizeAndHash("Quinn"), 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 }; } // Specifies whether user consent was obtained for the data you are uploading. See // https://www.google.com/about/company/user-consent-policy // for details. userDataWithPhysicalAddress.Consent = new Consent() { AdPersonalization = ConsentStatus.Granted, AdUserData = ConsentStatus.Denied }; // 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.V17.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\V17\GoogleAdsClient; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsClientBuilder; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsException; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsServerStreamDecorator; use Google\Ads\GoogleAds\Util\V17\GoogleAdsFailures; use Google\Ads\GoogleAds\Util\V17\ResourceNames; use Google\Ads\GoogleAds\V17\Common\Consent; use Google\Ads\GoogleAds\V17\Common\ItemAttribute; use Google\Ads\GoogleAds\V17\Common\OfflineUserAddressInfo; use Google\Ads\GoogleAds\V17\Common\StoreSalesMetadata; use Google\Ads\GoogleAds\V17\Common\StoreSalesThirdPartyMetadata; use Google\Ads\GoogleAds\V17\Common\TransactionAttribute; use Google\Ads\GoogleAds\V17\Common\UserData; use Google\Ads\GoogleAds\V17\Common\UserIdentifier; use Google\Ads\GoogleAds\V17\Enums\ConsentStatusEnum\ConsentStatus; use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobFailureReasonEnum\OfflineUserDataJobFailureReason; use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobStatusEnum\OfflineUserDataJobStatus; use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobTypeEnum\OfflineUserDataJobType; use Google\Ads\GoogleAds\V17\Errors\GoogleAdsError; use Google\Ads\GoogleAds\V17\Resources\OfflineUserDataJob; use Google\Ads\GoogleAds\V17\Services\AddOfflineUserDataJobOperationsRequest; use Google\Ads\GoogleAds\V17\Services\AddOfflineUserDataJobOperationsResponse; use Google\Ads\GoogleAds\V17\Services\Client\OfflineUserDataJobServiceClient; use Google\Ads\GoogleAds\V17\Services\CreateOfflineUserDataJobRequest; use Google\Ads\GoogleAds\V17\Services\CreateOfflineUserDataJobResponse; use Google\Ads\GoogleAds\V17\Services\GoogleAdsRow; use Google\Ads\GoogleAds\V17\Services\OfflineUserDataJobOperation; use Google\Ads\GoogleAds\V17\Services\RunOfflineUserDataJobRequest; use Google\Ads\GoogleAds\V17\Services\SearchGoogleAdsStreamRequest; 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: The consent status for ad personalization. private const AD_PERSONALIZATION_CONSENT = null; // Optional: The consent status for ad user data. private const AD_USER_DATA_CONSENT = 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::AD_PERSONALIZATION_CONSENT => GetOpt::OPTIONAL_ARGUMENT, ArgumentNames::AD_USER_DATA_CONSENT => GetOpt::OPTIONAL_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::AD_PERSONALIZATION_CONSENT] ? ConsentStatus::value($options[ArgumentNames::AD_PERSONALIZATION_CONSENT]) : self::AD_PERSONALIZATION_CONSENT, $options[ArgumentNames::AD_USER_DATA_CONSENT] ? ConsentStatus::value($options[ArgumentNames::AD_USER_DATA_CONSENT]) : self::AD_USER_DATA_CONSENT, $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 $adPersonalizationConsent the ad personalization consent status * @param int|null $adUserDataConsent the ad user data consent status * @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 $adPersonalizationConsent, ?int $adUserDataConsent, ?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, $adPersonalizationConsent, $adUserDataConsent, $itemId, $merchantCenterAccountId, $countryCode, $languageCode, $quantity ); // Issues an asynchronous request to run the offline user data job. $offlineUserDataJobServiceClient->runOfflineUserDataJob( RunOfflineUserDataJobRequest::build($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( CreateOfflineUserDataJobRequest::build($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 int|null $adPersonalizationConsent the ad personalization consent status * @param int|null $adUserDataConsent the ad user data consent status * @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, ?int $adPersonalizationConsent, ?int $adUserDataConsent, ?string $itemId, ?int $merchantCenterAccountId, ?string $countryCode, ?string $languageCode, ?int $quantity ) { // Constructs the operation for each transaction. $userDataJobOperations = self::buildOfflineUserDataJobOperations( $customerId, $conversionActionId, $adPersonalizationConsent, $adUserDataConsent, $itemId, $merchantCenterAccountId, $countryCode, $languageCode, $quantity ); // Issues a request to add the operations to the offline user data job. /** @var AddOfflineUserDataJobOperationsResponse $operationResponse */ $request = AddOfflineUserDataJobOperationsRequest::build( $offlineUserDataJobResourceName, $userDataJobOperations ); // (Optional) Enables partial failure and warnings. $request->setEnablePartialFailure(true)->setEnableWarnings(true); $response = $offlineUserDataJobServiceClient->addOfflineUserDataJobOperations($request); // Prints the status message if any partial failure error is returned. // NOTE: The details of each partial failure error are not printed here, you can refer to // the example HandlePartialFailure.php to learn more. if ($response->hasPartialFailureError()) { 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 ); } // Prints the number of warnings if any warnings are returned. You can access // details of each warning using the same approach you'd use for partial failure // errors. if ($response->hasWarning()) { // Extracts all the warning errors from the response details into a single // GoogleAdsFailure object. $warningFailure = GoogleAdsFailures::fromAnys($response->getWarning()->getDetails()); // Prints some information about the warnings encountered. printf( "Encountered %d warning(s).%s", count($warningFailure->getErrors()), 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 * @param int|null $adPersonalizationConsent the ad personalization consent status * @param int|null $adUserDataConsent the ad user data consent status * @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, ?int $adPersonalizationConsent, ?int $adUserDataConsent, ?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('dana@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' ]) ]); // Adds consent information if specified. if (!empty($adPersonalizationConsent) || !empty($adUserDataConsent)) { $consent = new Consent(); if (!empty($adPersonalizationConsent)) { $consent->setAdPersonalization($adPersonalizationConsent); } if (!empty($adUserDataConsent)) { $consent->setAdUserData($adUserDataConsent); } // Specifies whether user consent was obtained for the data you are uploading. See // https://www.google.com/about/company/user-consent-policy for details. $userDataWithEmailAddress->setConsent($consent); } // 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('Dana'), 'hashed_last_name' => self::normalizeAndHash('Quinn'), // 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( SearchGoogleAdsStreamRequest::build($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, ad_user_data_consent, ad_personalization_consent, ): """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. ad_user_data_consent: The consent status for ad user data for all members in the job. ad_personalization_consent: The personalization consent status for ad user data for all members in the job. """ # 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, item_id, merchant_center_account_id, country_code, language_code, quantity, ad_user_data_consent, ad_personalization_consent, ) # 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, ad_user_data_consent, ad_personalization_consent, ): """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. ad_user_data_consent: The consent status for ad user data for all members in the job. ad_personalization_consent: The personalization consent status for ad user data for all members in the job. """ # Construct some sample transactions. operations = 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, ad_user_data_consent, ad_personalization_consent, ) # Constructs a request with partial failure enabled to add the operations # to the offline user data job, and enable_warnings set to true to retrieve # warnings. request = client.get_type("AddOfflineUserDataJobOperationsRequest") request.resource_name = offline_user_data_job_resource_name request.enable_partial_failure = True request.enable_warnings = True request.operations = operations response = ( offline_user_data_job_service.add_offline_user_data_job_operations( request=request, ) ) # Print the error message for any partial failure error that is returned. if response.partial_failure_error: print_google_ads_failures(response.partial_failure_error) else: print( f"Successfully added {len(operations)} to the offline user data " "job." ) # Print the message for any warnings that are returned. if response.warning: print_google_ads_failures(response.warning) def print_google_ads_failures(client, status): """Prints the details for partial failure errors and warnings. Both partial failure errors and warnings are returned as Status instances, which include serialized GoogleAdsFailure objects. Here we deserialize each GoogleAdsFailure and print the error details it includes. Args: client: An initialized Google Ads API client. status: a google.rpc.Status instance. """ for detail in status.details: google_ads_failure = client.get_type("GoogleAdsFailure") # Retrieve the class definition of the GoogleAdsFailure instance # with type() in order to use the "deserialize" class method to parse # the detail string into a protobuf message instance. failure_instance = type(google_ads_failure).deserialize(detail.value) for error in failure_instance.errors: print( "A partial failure or warning at index " f"{error.location.field_path_elements[0].index} occurred.\n" f"Message: {error.message}\n" f"Code: {error.error_code}" ) 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, ad_user_data_consent, ad_personalization_consent, ): """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. ad_user_data_consent: The consent status for ad user data for all members in the job. ad_personalization_consent: The personalization consent status for ad user data for all members in the job. 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("dana@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") # Specifies whether user consent was obtained for the data you are # uploading. For more details, see: # https://www.google.com/about/company/user-consent-policy if ad_user_data_consent: user_data_with_email_address.consent.ad_user_data = ( client.enums.ConsentStatusEnum[ad_user_data_consent] ) if ad_personalization_consent: user_data_with_email_address.consent.ad_personalization = ( client.enums.ConsentStatusEnum[ad_personalization_consent] ) if custom_value: user_data_with_email_address.transaction_attribute.custom_value = ( custom_value ) # 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( "Dana" ) address_identifier.address_info.hashed_last_name = normalize_and_hash( "Quinn" ) # 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" ) # 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__": 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.", ) group = parser.add_mutually_exclusive_group(required=False) group.add_argument( "-k", "--custom_key", type=str, 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", ) group.add_argument( "-v", "--custom_value", type=str, 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.", ) parser.add_argument( "-d", "--ad_user_data_consent", type=str, choices=[e.name for e in googleads_client.enums.ConsentStatusEnum], help=( "The data consent status for ad user data for all members in " "the job." ), ) parser.add_argument( "-p", "--ad_personalization_consent", type=str, choices=[e.name for e in googleads_client.enums.ConsentStatusEnum], help=( "The personalization consent status for ad user data for all " "members in the job." ), ) args = parser.parse_args() # GoogleAdsClient will read the google-ads.yaml configuration file in the # home directory if none is specified. googleads_client = GoogleAdsClient.load_from_storage(version="v17") # 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, args.ad_user_data_consent, args.ad_personalization_consent, ) 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, ad_user_data_consent, ad_personalization_consent) # 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, ad_user_data_consent, ad_personalization_consent ) # 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, ad_user_data_consent, ad_personalization_consent) # Constructs the operation for each transaction. user_data_job_operations = build_offline_user_data_job_operations( client, customer_id, conversion_action_id, custom_value, ad_user_data_consent, ad_personalization_consent) # 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, enable_warnings: 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 end if response.warning # Convert to a GoogleAdsFailure. warnings = client.decode_warning(response.warning) puts "Encountered #{warnings.errors.size} warning(s)." end puts "Successfully added #{user_data_job_operations.size} operations to " \ "the offline user data job." 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, ad_user_data_consent, ad_personalization_consent) operations = [] # Creates the first transaction for upload based on an email address # and state. operations << client.operation.create_resource.offline_user_data_job do |op| op.user_identifiers << client.resource.user_identifier do |id| # Email addresses must be normalized and hashed. id.hashed_email = normalize_and_hash("dana@example.com") end op.user_identifiers << client.resource.user_identifier do |id| id.address_info = client.resource.offline_user_address_info do |info| info.state = "NY" end end op.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 if !ad_user_data_consent.nil? || !ad_personalization_consent.nil? op.consent = client.resource.consent do |c| # Specifies whether user consent was obtained for the data you are # uploading. For more details, see: # https://www.google.com/about/company/user-consent-policy unless ad_user_data_consent.nil? c.ad_user_data = ad_user_data_consent end unless ad_personalization_consent.nil? c.ad_personalization = ad_personalization_consent end end end end # Creates the second transaction for upload based on a physical address. operations << client.operation.create_resource.offline_user_data_job do |op| op.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("Dana") info.hashed_last_name = normalize_and_hash("Quinn") # Country code and zip code are sent in plain text. info.country_code = "US" info.postal_code = "10011" end end op.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.on('-d', '--ad-user-data-consent [AD-USER-DATA_CONSENT]', String, 'The personalization consent status for ad user data for all members in the job.' \ 'e.g. UNKNOWN, GRANTED, DENIED') do |v| options[:ad_user_data_consent] = v end opts.on('-p', '--ad-personalization-consent [AD-PERSONALIZATION-CONSENT]', String, 'The personalization consent status for ad user data for all members in the job.' \ 'e.g. UNKNOWN, GRANTED, DENIED') do |v| options[:ad_personalization_consent] = 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], options[:ad_user_data_consent], options[:ad_personalization_consent], ) 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::V17::Resources::OfflineUserDataJob; use Google::Ads::GoogleAds::V17::Common::Consent; use Google::Ads::GoogleAds::V17::Common::ItemAttribute; use Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo; use Google::Ads::GoogleAds::V17::Common::StoreSalesMetadata; use Google::Ads::GoogleAds::V17::Common::StoreSalesThirdPartyMetadata; use Google::Ads::GoogleAds::V17::Common::TransactionAttribute; use Google::Ads::GoogleAds::V17::Common::UserData; use Google::Ads::GoogleAds::V17::Common::UserIdentifier; use Google::Ads::GoogleAds::V17::Enums::OfflineUserDataJobTypeEnum qw(STORE_SALES_UPLOAD_FIRST_PARTY STORE_SALES_UPLOAD_THIRD_PARTY); use Google::Ads::GoogleAds::V17::Services::OfflineUserDataJobService::OfflineUserDataJobOperation; use Google::Ads::GoogleAds::V17::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 country code of the location associated with the # feed where your items are uploaded. Only required if uploading with item # attributes. my $country_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; # Optional: Specify the ad personalization consent status. my $ad_personalization_consent = undef; # Optional: Specify the ad user data consent status. my $ad_user_data_consent = undef; 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, $country_code, $language_code, $quantity, $$ad_personalization_consent, $ad_user_data_consent ) = @_; 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, $country_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::V17::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::V17::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::V17::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, $country_code, $language_code, $quantity ) = @_; # Construct the operation for each transaction. my $user_data_job_operations = build_offline_user_data_job_operations( $customer_id, $conversion_action_id, $custom_key, $item_id, $merchant_center_account_id, $country_code, $language_code, $quantity, $ad_personalization_consent, $ad_user_data_consent ); # 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", # Enable warnings (optional). enableWarnings => "true", operations => $user_data_job_operations }); # Print the status message if any partial failure error is returned. # Note: The details of each partial failure error are not printed here, you # can refer to the example handle_partial_failure.pl to learn more. if ($response->{partialFailureError}) { # Extract the partial failure from the response status. my $partial_failure = $response->{partialFailureError}{details}[0]; foreach my $error (@{$partial_failure->{errors}}) { printf "Partial failure occurred: '%s'\n", $error->{message}; } 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", scalar @{$partial_failure->{errors}}, scalar @$user_data_job_operations, $response->{partialFailureError}{message}; } else { printf "Successfully added %d operations to the offline user data job.\n", scalar @$user_data_job_operations; } # Print the number of warnings if any warnings are returned. You can access # details of each warning using the same approach you'd use for partial failure # errors. if ($response->{warning}) { # Extract the warnings from the response status. my $warnings_failure = $response->{warning}{details}[0]; printf "Encountered %d warning(s).\n", scalar @{$warnings_failure->{errors}}; } } # 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, $country_code, $language_code, $quantity, $ad_personalization_consent, $ad_user_data_consent ) = @_; # Create the first transaction for upload based on an email address and state. my $user_data_with_email_address = Google::Ads::GoogleAds::V17::Common::UserData->new({ userIdentifiers => [ Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({ # Hash normalized email addresses based on SHA-256 hashing algorithm. hashedEmail => normalize_and_hash('dana@example.com')} ), Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({ addressInfo => Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo->new({ state => "NY" })}) ], transactionAttribute => Google::Ads::GoogleAds::V17::Common::TransactionAttribute->new({ conversionAction => Google::Ads::GoogleAds::V17::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", })}); # Add consent information if specified. if ($ad_personalization_consent or $ad_user_data_consent) { # Specify whether user consent was obtained for the data you are uploading. # See https://www.google.com/about/company/user-consent-policy for details. $user_data_with_email_address->{consent} = Google::Ads::GoogleAds::V17::Common::Consent({ adPersonalization => $ad_personalization_consent, adUserData => $ad_user_data_consent }); } # 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::V17::Common::UserData->new({ userIdentifiers => [ Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({ addressInfo => Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo->new({ # First and last name must be normalized and hashed. hashedFirstName => normalize_and_hash("Dana"), hashedLastName => normalize_and_hash("Quinn"), # Country code and zip code are sent in plain text. countryCode => "US", postalCode => "10011" })}) ], transactionAttribute => Google::Ads::GoogleAds::V17::Common::TransactionAttribute->new({ conversionAction => Google::Ads::GoogleAds::V17::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::V17::Common::ItemAttribute->new({ itemId => $item_id, merchantId => $merchant_center_account_id, countryCode => $country_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::V17::Services::OfflineUserDataJobService::OfflineUserDataJobOperation ->new({ create => $user_data_with_email_address } ), Google::Ads::GoogleAds::V17::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, "country_code