In addition to
uploading conversions by GCLID,
the Google Ads API also supports uploading enhanced conversions for
leads. Follow the same
process you would for uploading conversions by GCLID, but make the following
changes when populating each
ClickConversion
:
Instead of setting
gclid
, populate theuser_identifiers
with the standardized and hashed email address or phone number of the user. If available, you may add both values in separateUserIdentifiers
on the sameClickConversion
.Set the
conversion_action
to a resource name of aConversionAction
with atype
ofUPLOAD_CLICKS
.Do not set
external_attribution_data
or specify aconversion_action
that uses an external attribution model. Google Ads does not support externally attributed conversions for uploads using identifiers.
In order to ensure full and accurate conversion reporting when uploading offline conversions with enhanced conversions for leads, you must upload all available offline conversion events, including those that might not have came from Google Ads. This differs from uploading offline conversions using GCLIDs where you only upload the subset of offline conversion events with a GCLID.
Uploading all conversion events will lead to CLICK_NOT_FOUND
errors for any
events that are not from Google Ads. Since these errors are expected when uploading
all conversion events,
UploadClickConversionsRequest
has a debug_enabled
field.
If
debug_enabled
isfalse
or not set, the Google Ads API only performs basic input validation, skips subsequent upload checks, and returns success even if no click is found for the provideduser_identifiers
.This is the default.
If
debug_enabled
istrue
, the Google Ads API performs all validations and returns aCLICK_NOT_FOUND
error for anyClickConversion
where there is no Google Ads conversion for the provideduser_identifiers
.
During development and testing, you can set debug_enabled
to true
to help
identify issues. For example, if you have a set of conversions and
user_identifiers
that you know are from Google Ads conversions, you can use
the true
setting to validate that those uploads do not result in a
CLICK_NOT_FOUND
error. However, when you proceed past development and
testing, we recommend setting debug_enabled
to false
to avoid excessive
errors.
Setup
Complete the following steps before uploading enhanced conversions for leads.
Confirm that you have accepted the customer data terms in the effective conversion account and the account you'll be specifying with the
customer_id
of your request. Also confirm that you have opted-in the effective conversion account for enhanced conversions for leads.You can use the Google Ads UI for these checks, but if you'd prefer to use the Google Ads API, retrieve the
conversion_tracking_setting
of your accounts using thesearchStream
orsearch
method ofGoogleAdsService
and the following query:SELECT customer.id, customer.conversion_tracking_setting.accepted_customer_data_terms, customer.conversion_tracking_setting.enhanced_conversions_for_leads_enabled FROM customer
Then verify that both
accepted_customer_data_terms
andenhanced_conversions_for_leads_enabled
aretrue
.Configure Google Tag Manager or the Google tag using the steps outlined in the Google Ads help center.
Normalization and hashing
For privacy concerns, email addresses and phone numbers must be hashed using the SHA-256 algorithm before being uploaded. In order to standardize the hash results, prior to hashing one of these values you must:
- Remove leading/trailing whitespaces.
- Convert the text to lowercase.
- Format phone numbers according to the E164 standard.
- Remove all periods (
.
) that precede the domain name ingmail.com
andgooglemail.com
email addresses.
Java
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(); } /** * Returns the result of normalizing and hashing an email address. For this use case, Google Ads * requires removal of any '.' characters preceding {@code gmail.com} or {@code googlemail.com}. * * @param digest the digest to use to hash the normalized string. * @param emailAddress the email address to normalize and hash. */ private String normalizeAndHashEmailAddress(MessageDigest digest, String emailAddress) throws UnsupportedEncodingException { String normalizedEmail = emailAddress.toLowerCase(); String[] emailParts = normalizedEmail.split("@"); if (emailParts.length > 1 && emailParts[1].matches("^(gmail|googlemail)\\.com\\s*")) { // Removes any '.' characters from the portion of the email address before the domain if the // domain is gmail.com or googlemail.com. emailParts[0] = emailParts[0].replaceAll("\\.", ""); normalizedEmail = String.format("%s@%s", emailParts[0], emailParts[1]); } return normalizeAndHash(digest, normalizedEmail); }
C#
/// <summary> /// Normalizes the email address and hashes it. For this use case, Google Ads requires /// removal of any '.' characters preceding <code>gmail.com</code> or /// <code>googlemail.com</code>. /// </summary> /// <param name="emailAddress">The email address.</param> /// <returns>The hash code.</returns> private string NormalizeAndHashEmailAddress(string emailAddress) { string normalizedEmail = emailAddress.ToLower(); string[] emailParts = normalizedEmail.Split('@'); if (emailParts.Length > 1 && (emailParts[1] == "gmail.com" || emailParts[1] == "googlemail.com")) { // Removes any '.' characters from the portion of the email address before // the domain if the domain is gmail.com or googlemail.com. emailParts[0] = emailParts[0].Replace(".", ""); normalizedEmail = $"{emailParts[0]}@{emailParts[1]}"; } return NormalizeAndHash(normalizedEmail); } /// <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(); }
PHP
private static function normalizeAndHash(string $hashAlgorithm, string $value): string { return hash($hashAlgorithm, strtolower(trim($value))); } /** * Returns the result of normalizing and hashing an email address. For this use case, Google * Ads requires removal of any '.' characters preceding "gmail.com" or "googlemail.com". * * @param string $hashAlgorithm the hash algorithm to use * @param string $emailAddress the email address to normalize and hash * @return string the normalized and hashed email address */ private static function normalizeAndHashEmailAddress( string $hashAlgorithm, string $emailAddress ): string { $normalizedEmail = strtolower($emailAddress); $emailParts = explode("@", $normalizedEmail); if ( count($emailParts) > 1 && preg_match('/^(gmail|googlemail)\.com\s*/', $emailParts[1]) ) { // Removes any '.' characters from the portion of the email address before the domain // if the domain is gmail.com or googlemail.com. $emailParts[0] = str_replace(".", "", $emailParts[0]); $normalizedEmail = sprintf('%s@%s', $emailParts[0], $emailParts[1]); } return self::normalizeAndHash($hashAlgorithm, $normalizedEmail); }
Python
def normalize_and_hash_email_address(email_address): """Returns the result of normalizing and hashing an email address. For this use case, Google Ads requires removal of any '.' characters preceding "gmail.com" or "googlemail.com" Args: email_address: An email address to normalize. Returns: A normalized (lowercase, removed whitespace) and SHA-265 hashed string. """ normalized_email = email_address.lower() email_parts = normalized_email.split("@") # Checks whether the domain of the email address is either "gmail.com" # or "googlemail.com". If this regex does not match then this statement # will evaluate to None. is_gmail = re.match(r"^(gmail|googlemail)\.com$", email_parts[1]) # Check that there are at least two segments and the second segment # matches the above regex expression validating the email domain name. if len(email_parts) > 1 and is_gmail: # Removes any '.' characters from the portion of the email address # before the domain if the domain is gmail.com or googlemail.com. email_parts[0] = email_parts[0].replace(".", "") normalized_email = "@".join(email_parts) return normalize_and_hash(normalized_email) def normalize_and_hash(s): """Normalizes and hashes a string with SHA-256. Private customer data must be hashed during upload, as described at: https://support.google.com/google-ads/answer/7474263 Args: s: The string to perform this operation on. Returns: A normalized (lowercase, removed whitespace) and SHA-256 hashed string. """ return hashlib.sha256(s.strip().lower().encode()).hexdigest()
Ruby
# 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/7474263. def normalize_and_hash(str) # Remove leading and trailing whitespace and ensure all letters are lowercase # before hasing. Digest::SHA256.hexdigest(str.strip.downcase) end # Returns the result of normalizing and hashing an email address. For this use # case, Google Ads requires removal of any '.' characters preceding 'gmail.com' # or 'googlemail.com'. def normalize_and_hash_email(email) email_parts = email.downcase.split("@") # Removes any '.' characters from the portion of the email address before the # domain if the domain is gmail.com or googlemail.com. if email_parts.last =~ /^(gmail|googlemail)\.com\s*/ email_parts[0] = email_parts[0].gsub('.', '') end normalize_and_hash(email_parts.join('@')) end
Perl
sub normalize_and_hash { my $value = shift; $value =~ s/^\s+|\s+$//g; return sha256_hex(lc $value); } # Returns the result of normalizing and hashing an email address. For this use # case, Google Ads requires removal of any '.' characters preceding 'gmail.com' # or 'googlemail.com'. sub normalize_and_hash_email_address { my $email_address = shift; my $normalized_email = lc $email_address; my @email_parts = split('@', $normalized_email); if (scalar @email_parts > 1 && $email_parts[1] =~ /^(gmail|googlemail)\.com\s*/) { # Remove any '.' characters from the portion of the email address before the # domain if the domain is 'gmail.com' or 'googlemail.com'. $email_parts[0] =~ s/\.//g; $normalized_email = sprintf '%s@%s', $email_parts[0], $email_parts[1]; } return normalize_and_hash($normalized_email); }
Upload conversions code example
The following snippet demonstrates how to construct a conversion upload that contains an identifier for email address, with standardization and hashing applied as required.
Java
// Gets the conversion action resource name. String conversionActionResourceName = ResourceNames.conversionAction(customerId, conversionActionId); // Creates a builder for constructing the click conversion. ClickConversion.Builder clickConversionBuilder = ClickConversion.newBuilder() .setConversionAction(conversionActionResourceName) .setConversionDateTime(conversionDateTime) .setConversionValue(conversionValue) .setCurrencyCode("USD"); // Sets the order ID if provided. if (orderId != null) { clickConversionBuilder.setOrderId(orderId); } // Creates a SHA256 message digest for hashing user identifiers in a privacy-safe way, as // described at https://support.google.com/google-ads/answer/9888656. MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256"); // Creates a user identifier using the hashed email address, using the normalize and hash method // specifically for email addresses. // If using a phone number, use the normalizeAndHash(String) method instead. String hashedEmail = normalizeAndHashEmailAddress(sha256Digest, emailAddress); UserIdentifier userIdentifier = UserIdentifier.newBuilder() .setHashedEmail(hashedEmail) // Optional: Specifies the user identifier source. .setUserIdentifierSource(UserIdentifierSource.FIRST_PARTY) .build(); // Adds the user identifier to the conversion. clickConversionBuilder.addUserIdentifiers(userIdentifier); // Calls build to build the conversion. ClickConversion clickConversion = clickConversionBuilder.build();
C#
// Gets the conversion action resource name. string conversionActionResourceName = ResourceNames.ConversionAction(customerId, conversionActionId); // Creates a builder for constructing the click conversion. ClickConversion clickConversion = new ClickConversion() { ConversionAction = conversionActionResourceName, ConversionDateTime = conversionDateTime, ConversionValue = conversionValue, CurrencyCode = "USD" }; // Sets the order ID if provided. if (!string.IsNullOrEmpty(orderId)) { clickConversion.OrderId = orderId; } // Optional: Specifies the user identifier source. clickConversion.UserIdentifiers.Add(new UserIdentifier() { // Creates a user identifier using the hashed email address, using the normalize // and hash method specifically for email addresses. // If using a phone number, use the NormalizeAndHash(String) method instead. HashedEmail = NormalizeAndHashEmailAddress(emailAddress), // Optional: Specifies the user identifier source. UserIdentifierSource = UserIdentifierSource.FirstParty });
PHP
// Creates a click conversion with the specified attributes. $clickConversion = new ClickConversion([ 'conversion_action' => ResourceNames::forConversionAction($customerId, $conversionActionId), 'conversion_date_time' => $conversionDateTime, 'conversion_value' => $conversionValue, 'currency_code' => 'USD' ]); // Sets the order ID if provided. if ($orderId !== null) { $clickConversion->setOrderId($orderId); } // Uses the SHA-256 hash algorithm for hashing user identifiers in a privacy-safe way, as // described at https://support.google.com/google-ads/answer/9888656. $hashAlgorithm = "sha256"; // Creates a user identifier to store the hashed email address. $userIdentifier = new UserIdentifier([ // Use the normalizeAndHash() method if a phone number is specified instead of the email // address. 'hashed_email' => self::normalizeAndHashEmailAddress($hashAlgorithm, $emailAddress), // Optional: Specifies the user identifier source. 'user_identifier_source' => UserIdentifierSource::FIRST_PARTY ]); // Adds the user identifier to the conversion. $clickConversion->setUserIdentifiers([$userIdentifier]);
Python
conversion_action_service = client.get_service("ConversionActionService") # Gets the conversion action resource name. conversion_action_resource_name = conversion_action_service.conversion_action_path( customer_id, conversion_action_id ) click_conversion = client.get_type("ClickConversion") click_conversion.conversion_action = conversion_action_resource_name click_conversion.conversion_date_time = conversion_date_time click_conversion.conversion_value = conversion_value click_conversion.currency_code = "USD" # Sets the order ID if provided. if order_id: click_conversion.order_id = order_id # Creates a user identifier using the hashed email address, using the # normalize and hash method specifically for email addresses. If using a # phone number, use the "_normalize_and_hash" method instead. user_identifier = client.get_type("UserIdentifier") # Creates a SHA256 hashed string using the given email address, as # described at https://support.google.com/google-ads/answer/9888656. user_identifier.hashed_email = normalize_and_hash_email_address( email_address ) # Optional: Specifies the user identifier source. user_identifier.user_identifier_source = ( client.enums.UserIdentifierSourceEnum.FIRST_PARTY ) # Adds the user identifier to the conversion. click_conversion.user_identifiers.append(user_identifier)
Ruby
click_conversion = client.resource.click_conversion do |cc| cc.conversion_action = client.path.conversion_action(customer_id, conversion_action_id) cc.conversion_date_time = conversion_date_time cc.conversion_value = conversion_value.to_f cc.currency_code = 'USD' unless order_id.nil? cc.order_id = order_id end # Creates a user identifier using the hashed email address, using the # normalize and hash method specifically for email addresses. # If using a phone number, use the normalize_and_hash method instead. cc.user_identifiers << client.resource.user_identifier do |id| id.hashed_email = normalize_and_hash_email(email_address) # Optional: Specifies the user identifier source. id.user_identifier_source = :FIRST_PARTY end end
Perl
# Construct the click conversion. my $click_conversion = Google::Ads::GoogleAds::V13::Services::ConversionUploadService::ClickConversion ->new({ conversionAction => Google::Ads::GoogleAds::V13::Utils::ResourceNames::conversion_action( $customer_id, $conversion_action_id ), conversionDateTime => $conversion_date_time, conversionValue => $conversion_value, currencyCode => "USD" }); # Set the order ID if provided. if (defined $order_id) { $click_conversion->{orderId} = $order_id; } # Create a user identifier using the hashed email address, using the normalize # and hash method specifically for email addresses. # If using a phone number, use the normalize_and_hash() method instead. my $hashed_email = normalize_and_hash_email_address($email_address); my $user_identifier = Google::Ads::GoogleAds::V13::Common::UserIdentifier->new({ hashedEmail => $hashed_email, # Optional: Specify the user identifier source. userIdentifierSource => FIRST_PARTY }); # Add the user identifier to the conversion. $click_conversion->{userIdentifiers} = [$user_identifier];
Common errors
ConversionUploadError.CLICK_NOT_FOUND
- No click was found that matched the provided user identifiers.
ConversionUploadError.INVALID_USER_IDENTIFIER
- A
user_identifier
for a field that requires hashing was not hashed using the SHA-256 algorithm. ConversionUploadError.EXTERNALLY_ATTRIBUTED_CONVERSION_ACTION_NOT_PERMITTED_WITH_USER_IDENTIFIER
- The
conversion_action
specified uses an external attribution model. ConversionUploadError.UNSUPPORTED_USER_IDENTIFIER
- A
user_identifier
of the conversion contains a value that is not one of the allowed identifiers (hashed_email_address
andhashed_phone_number
). CollectionSizeError.TOO_MANY
- The
user_identifiers
collection contains more than five elements.