Upload Enhanced Conversions For Leads

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 the user_identifiers with the standardized and hashed email address or phone number of the user. If available, you may add both values in separate UserIdentifiers on the same ClickConversion.

  • Set the conversion_action to a resource name of a ConversionAction with a type of UPLOAD_CLICKS.

  • Do not set external_attribution_data or specify a conversion_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 is false 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 provided user_identifiers.

    This is the default.

  • If debug_enabled is true, the Google Ads API performs all validations and returns a CLICK_NOT_FOUND error for any ClickConversion where there is no Google Ads conversion for the provided user_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.

  1. 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 the searchStream or search method of GoogleAdsService 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 and enhanced_conversions_for_leads_enabled are true.

  2. 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 in gmail.com and googlemail.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 and hashed_phone_number).
CollectionSizeError.TOO_MANY
The user_identifiers collection contains more than five elements.