> [!WARNING]
> **Warning:** Since **June 15, 2026** , [`UploadClickConversions`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadService/UploadClickConversions) requests fail with a `CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE` error for developer tokens that sent no offline conversion upload requests between December 17, 2025, and June 15, 2026. Use the [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) instead. See [Feature deprecations](https://developers.google.com/google-ads/api/docs/deprecations#feature-deprecations) for details. Although developer tokens were [deprecated in favor of Google Cloud projects](https://developers.google.com/google-ads/api/docs/api-policy/developer-token) and sunset on September 9, 2026, this historical access restriction remains in effect.

You can use the Google Ads API to import offline conversions into Google Ads to track ads
that led to sales in the offline world, such as over the phone or through a
sales rep.

In order to fully take advantage of the benefits of importing conversion data,
we recommend implementing enhanced conversions for leads, which leverages GCLIDs
and user-provided data to maximize durability and performance.

> [!NOTE]
> **Note:** If you're unable to gather and import user-provided data, you can implement legacy offline conversion imports by following the [Legacy Offline Conversion Imports Guide](https://developers.google.com/google-ads/api/docs/conversions/legacy_oci_guide).

## Enhanced conversions

Enhanced conversions help you to improve the accuracy of your conversion
measurement by supplementing your conversions with first-party conversion data,
like email address, phone number, and mailing address.

There are two types of enhanced conversions. For more details, see
[About enhanced conversions](https://support.google.com/google-ads/answer/9888656).

The following section explains how to enhance *offline* conversions, a feature
also referred to as **enhanced conversions for leads**.

### What is enhanced conversions for leads?

Enhanced conversions for leads is an upgraded version of offline conversion
import that uses user-provided data, such as email addresses, to supplement
imported offline conversion data to improve accuracy and bidding performance.
When you import your offline conversions, the provided hashed customer data is
used to attribute back to the Google Ads campaign by matching to the same data
collected on your website, such as a lead form, and to signed-in customers who
engaged with your ad. For more information, see
[About enhanced conversions for leads](https://support.google.com/google-ads/answer/15713840).

There are two ways to implement enhanced conversions for leads depending on
whether you use the [Google tag](https://support.google.com/google-ads/answer/11021502) to track form submission events on
your webpage. For the best performance and durability, we recommend using Google
tag for enhanced conversions for leads.

- If you are starting from scratch, start with the [Prerequisites](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#prerequisites) section.
- If you already have offline conversion imports set up, and want to upgrade to enhanced conversions for leads, we recommend starting with the [Configure tagging](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#tagging) section.
- If you've already set up the Google tag, or don't plan to use the Google tag, and are starting work on your Google Ads API integration, jump to the [API implementation](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#ecl-implementation) section.
- If you are unable to import user-provided data, or rely on external attribution for your conversions, see the [Legacy Offline Conversion Imports Guide](https://developers.google.com/google-ads/api/docs/conversions/legacy_oci_guide).

### Prerequisites

First, verify that you've completed the steps in our
[getting started section](https://developers.google.com/google-ads/api/docs/conversions/getting-started).

You must opt in to enhanced conversions and accept the customer data terms
before you can use enhanced conversions for leads. You can verify whether these
prerequisites are already met by issuing the following query to the Google Ads
conversion customer:

    SELECT
      customer.id,
      customer.conversion_tracking_setting.accepted_customer_data_terms,
      customer.conversion_tracking_setting.enablement_setting,
      customer.conversion_tracking_setting.effective_enablement_setting
    FROM customer

If `accepted_customer_data_terms` is `false`, or if `enablement_setting`
(or `effective_enablement_setting` when using cross-account conversion
tracking) is not [`ON`](https://developers.google.com/google-ads/api/reference/rpc/v26/EnhancedConversionsConfigEnum.EnhancedConversionsConfig), follow the instructions
in [Create a new conversion action](https://support.google.com/google-ads/answer/11021502#create) to complete these
prerequisites.

### Configure tagging

Configure the Google tag to enable enhanced conversions for leads by following
the instructions to [Configure Google tag settings](https://support.google.com/google-ads/answer/11021502#configure). To
set up enhanced conversions for leads with Google Tag Manager, follow these
[Google Tag Manager instructions](https://support.google.com/google-ads/answer/11347292).

## API implementation

Here is the overall flow for importing enhanced conversions for leads using the
Google Ads API.

1. [Normalize and hash the user-provided data](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#prepare-data), such as email
   address and phone number.

2. [Populate `ClickConversion` objects](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#populate-click-conversions) with the
   normalized and hashed user-provided data.

3. [Import `ClickConversion` objects](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#import-the-request) to the Google Ads API using
   the [`ConversionUploadService`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadService). See
   [`UploadClickConversions`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadService/UploadClickConversions) for more details.

4. [Review your imports](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#review).

### Request and ClickConversion schema summary

The following table summarizes the key fields on
[`UploadClickConversionsRequest`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsRequest) and
[`ClickConversion`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion) for enhanced conversions for leads (see
[Populate `ClickConversion` objects](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#populate-click-conversions) and
[Construct the request](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#construct-the-request) for full field details):

| Field path | Requirement | Format and constraints |
|---|---|---|
| `customer_id` | Required | Set to the **Google Ads conversion customer** ID (`google_ads_conversion_customer` on `customer.conversion_tracking_setting`), without hyphens. |
| `partial_failure` | Required | Must be set to `true` so valid conversions are imported and row-level errors are returned in [`partial_failure_error`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsResponse#partial_failure_error). |
| `validate_only` | Optional | When `true`, validates the request without importing conversions (returns any validation errors in [`partial_failure_error`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsResponse#partial_failure_error)). |
| `job_id` | Optional | Positive `int32` (`1` to `2^31 - 1`) to group multiple requests under one logical job in [`job_summaries`](https://developers.google.com/google-ads/api/reference/rpc/v26/OfflineConversionUploadClientSummary#job_summaries). |
| `conversions[].conversion_action` | Required | Resource name (`customers/{conversion_customer_id}/conversionActions/{action_id}`) of an `ENABLED` conversion action with `type = UPLOAD_CLICKS`. |
| `conversions[].conversion_date_time` | Required | Must be after the click time and include a timezone offset: `yyyy-mm-dd HH:mm:ss+|-HH:mm` (for example, `2026-06-01 14:30:00-04:00`). |
| `conversions[].conversion_value` and `currency_code` | Recommended | Numeric conversion value (`double`) and 3-character ISO 4217 [currency code](https://developers.google.com/google-ads/api/data/codes-formats#currency_codes) (for example, `USD`). |
| `conversions[].order_id` | Recommended | Unique transaction ID per conversion action to prevent duplicate imports and support future [adjustments](https://developers.google.com/google-ads/api/docs/conversions/upload-adjustments). |
| `conversions[].gclid` / `gbraid` / `wbraid` | Recommended when available | Click identifiers captured from the landing page URL. Set every identifier that you captured. When several identifiers are present, Google Ads uses `gclid`, then `wbraid`, then `gbraid`. Include `gclid` alongside `user_identifiers` whenever available. If you set `wbraid` or `gbraid` without a `gclid`, Google Ads ignores the `user_identifiers` field. |
| `conversions[].user_identifiers[]` | Required for enhanced conversions for leads | One to five [`UserIdentifier`](https://developers.google.com/google-ads/api/reference/rpc/v26/UserIdentifier) objects per conversion. Set the optional `user_identifier_source` field (such as `FIRST_PARTY`) and either the `hashed_email` field (trimmed, lowercased, dots and `+` suffixes before `@` removed for `gmail.com` and `googlemail.com`, SHA-256 hex) or the `hashed_phone_number` field ([E.164](https://en.wikipedia.org/wiki/E.164) format, SHA-256 hex). See [Normalize and hash user-provided data](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#prepare-data). The `address_info` field is not supported. |
| `conversions[].consent.ad_user_data` | Recommended | Set to `GRANTED` or `DENIED` on the [`Consent`](https://developers.google.com/google-ads/api/reference/rpc/v26/Consent) object. (The `ad_personalization` field is supported only on the `OfflineUserDataJobService` and `UserDataService` services.) |
| `conversions[].custom_variables` and `external_attribution_data` | Do not set with `user_identifiers` | Do not set the [`custom_variables`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion#custom_variables) or `external_attribution_data` fields on conversions that contain `user_identifiers`, or Google Ads ignores the user-provided data. Conversions with only `user_identifiers` for externally attributed conversion actions return an `EXTERNALLY_ATTRIBUTED_CONVERSION_ACTION_NOT_PERMITTED_WITH_USER_IDENTIFIER` error. |

### Retrieve your conversion action resource name

To upload conversions, you must specify the resource name of the
[`ConversionAction`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionAction) you've set up in Google Ads for importing
click conversions. You can find the resource name by querying the
[`conversion_action`](https://developers.google.com/google-ads/api/fields/v26/conversion_action) resource using the following GAQL
query:

    SELECT
      customer.id,
      conversion_action.id,
      conversion_action.name,
      conversion_action.type,
      conversion_action.resource_name
    FROM conversion_action
    WHERE conversion_action.type = 'UPLOAD_CLICKS'
      AND conversion_action.status = 'ENABLED'

### Normalize and hash user-provided data

For privacy considerations, the following data must be hashed using SHA-256
before being imported:

- Email address
- Phone number
- First name (for the [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) and the `ConversionAdjustmentUploadService` service)
- Last name (for the [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) and the `ConversionAdjustmentUploadService` service)
- Street address (for the [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) and the `ConversionAdjustmentUploadService` service)

*Do not hash* the following mailing address fields (used with the
[Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) and the `ConversionAdjustmentUploadService` service):

- Country
- State
- City
- Zip code

> [!NOTE]
> **Note:** When importing offline click conversions with the [`ConversionUploadService.UploadClickConversions`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadService/UploadClickConversions) method, the [`UserIdentifier`](https://developers.google.com/google-ads/api/reference/rpc/v26/UserIdentifier) object supports only the `hashed_email` and `hashed_phone_number` fields (the `address_info` field is not supported). The [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline) and the `ConversionAdjustmentUploadService` service support mailing address fields for enhanced conversions for web.

In order to standardize the hash results, do the following prior to hashing each
of these values:

- Remove leading and trailing whitespaces.
- Convert the text to lowercase.
- Format phone numbers according to the [E.164 standard](https://en.wikipedia.org/wiki/E.164).

#### Email address formatting for enhanced conversions

To ensure the highest match rates for enhanced conversions, you must properly
format and hash email addresses using the following steps:

1. **Apply standard normalization (all email addresses):**

   - Remove any leading and trailing ASCII whitespace.
   - Convert all characters in the email address to lowercase.
2. **Apply domain-specific normalization (`gmail.com` and `googlemail.com`
   only):**

   - After lowercasing the email address, check whether the domain (after the `@` symbol) is `gmail.com` or `googlemail.com`.
   - **Remove periods (`.`):** From the username portion (before the `@` symbol), remove all period characters.
   - **Remove plus suffixes (`+`):** From the username portion, remove the first plus sign (`+`) and all subsequent characters up to the `@` symbol.
     - Example: `Jane.Doe+Shopping@googlemail.com` first becomes `jane.doe+shopping@googlemail.com` (lowercase), and then `janedoe@googlemail.com` (Gmail and Googlemail rules).
   - **Other domains:** For any domain *other than* `gmail.com` or `googlemail.com`, do *not* remove periods or plus suffixes from the username. Only apply standard normalization (trim whitespace and convert to lowercase).
     - Example: `user.name+NYC@Example.com` is normalized to `user.name+nyc@example.com`.
3. **Hash the normalized email address:**

   - Encode the normalized email string as **UTF-8** bytes.
   - Hash the UTF-8 bytes using the **SHA-256** algorithm.
   - Format the resulting digest as a **lowercase hexadecimal string** (64 characters).

> [!NOTE]
> **Note:** The email normalization requirements for enhanced conversions are distinct from how Google's system internally handles email variations for Customer Match. For enhanced conversions, you must perform the domain-specific normalization for `gmail.com` and `googlemail.com` (removing periods and plus suffixes from the username) *before* you hash the data. Failing to do so produces different hash values than Google expects for these domains, leading to missed matches.

    import hashlib
    import re


    def normalize_and_hash_email(email: str) -> str:
        """Normalizes and SHA-256 hashes an email address for enhanced conversions."""
        normalized = email.strip().lower()
        parts = normalized.split("@")
        if len(parts) != 2:
            raise ValueError("Invalid email address format.")
        username, domain = parts
        if domain in ("gmail.com", "googlemail.com"):
            username = username.replace(".", "")
            username = re.sub(r"\+.*$", "", username)
        normalized = f"{username}@{domain}"
        return hashlib.sha256(normalized.encode("utf-8")).hexdigest()

#### Code example

This example shows how to normalize and hash user-provided data.


### Java

```java
private String normalizeAndHash(MessageDigest digest, String s)
    throws UnsupportedEncodingException {
  // Normalizes by first converting all characters to lowercase, then trimming spaces.
  String normalized = s.toLowerCase();
  // Removes leading, trailing, and intermediate spaces.
  normalized = normalized.replaceAll("\\s+", "");
  // 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 or trailing '+' and characters that follow it from the
 * username portion of the email address if the domain is {@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 {
  // Removes all whitespace (leading, trailing, and intermediate) from the email address.
  String normalizedEmail = emailAddress.toLowerCase().replaceAll("\\s+", "");
  String[] emailParts = normalizedEmail.split("@", 2);
  if (emailParts.length == 2 && emailParts[1].matches("^(gmail|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].replaceAll("\\.", "");
    // Removes any '+' and all characters that follow it 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);
}

/**
 * Returns the result of normalizing and hashing a phone number. For this use case, Google Ads
 * requires phone numbers to be in E.164 format.
 *
 * @param digest the digest to use to hash the normalized string.
 * @param phoneNumber the phone number to normalize and hash.
 */
private String normalizeAndHashPhoneNumber(MessageDigest digest, String phoneNumber)
    throws UnsupportedEncodingException {
  // Removes non-digit characters and prepends a leading '+' sign.
  String digitsOnly = phoneNumber.replaceAll("[^0-9]", "");
  String formattedPhone = "+" + digitsOnly;
  if (!formattedPhone.matches("^\\+[1-9]\\d{6,14}$")) {
    throw new IllegalArgumentException("Phone number must be in E.164 format: " + phoneNumber);
  }
  return normalizeAndHash(digest, formattedPhone);
}
      
```

### C#

```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

```php
private static function normalizeAndHash(string $hashAlgorithm, string $value): string
{
    // Normalizes by first converting all characters to lowercase, then trimming spaces.
    $normalized = strtolower($value);
    // Removes leading, trailing, and intermediate spaces.
    $normalized = str_replace(' ', '', $normalized);
    return hash($hashAlgorithm, strtolower(trim($normalized)));
}

/**
 * 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

```python
def normalize_and_hash_email_address(email_address: str) -> str:
    """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: str = email_address.strip().lower()
    email_parts: list[str] = normalized_email.split("@")

    # Check that there are at least two segments
    if len(email_parts) > 1:
        # Removes any '.' and '+' characters from the portion of the email address
        # before the domain
        chars_to_remove = ".+"
        translation_table = str.maketrans("", "", chars_to_remove)
        email_parts[0] = email_parts[0].translate(translation_table)
        normalized_email = "@".join(email_parts)

    return normalize_and_hash(normalized_email)


def normalize_and_hash(s: str) -> str:
    """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

```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 hashing.
  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

```perl
sub normalize_and_hash {
  my $value = shift;

  # Removes leading, trailing, and intermediate spaces.
  $value =~ 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);
}
      
```

### curl

> [!NOTE]
> **Note:** While a direct REST code sample for this step isn't provided here, you can achieve this using a manual REST request.   
>
> Refer to the Google Ads API REST interface documentation and the method-specific reference pages. You will need to construct the JSON payload based on the proto definitions.   
>
> Key Resources:
>
> - [Using REST](https://developers.google.com/google-ads/api/rest/overview)
> - [REST Interface Structure](https://developers.google.com/google-ads/api/rest/design/overview)
> - [JSON Field Mappings](https://developers.google.com/google-ads/api/rest/design/json-mappings)
> - Consult the [REST API Reference](https://developers.google.com/google-ads/api/reference/rpc/latest/overview) for the specific service and method.

<br />

### Populate ClickConversion objects

The collection of [`ClickConversion`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion) objects in your
`UploadClickConversionsRequest` represents the set of conversions you want to
import. Keep the following details in mind when creating `ClickConversion`
objects:

`gclid`

:   [GCLIDs](https://support.google.com/google-ads/answer/9744275) are identifiers that are captured from URL parameters when an
    individual clicks on your ad and navigates to your website.

    > [!IMPORTANT]
    > **Important:** We recommend including GCLIDs in your import requests where possible, even if you have [Google tag set up to track enhanced conversions for leads](https://support.google.com/google-ads/answer/11021502).

`gbraid`

:   A [`gbraid`](https://support.google.com/google-ads/answer/10417364) is a URL parameter that is present when a user clicks on
    an ad on the web and is directed to your iOS app.

`wbraid`

:   A [`wbraid`](https://support.google.com/google-ads/answer/10417364) is a URL parameter that is present when a user clicks on
    an ad in an iOS app and is directed to your webpage.

> [!NOTE]
> **Note:** Include every click identifier that you captured for a conversion (`gclid`, `gbraid`, and `wbraid`). The API accepts any combination of these fields on the same `ClickConversion` object. When more than one identifier is present, Google Ads uses them in the following order of precedence: `gclid`, then `wbraid`, then `gbraid`. When Google Ads uses `wbraid` or `gbraid` (because the `gclid` field is not set), Google Ads ignores any `user_identifiers` on the conversion.

`cart_data`
:   Contains item-level details about a conversion. Populating the `cart_data`
    field is required to report on basket data---such as items purchased, price, and
    quantity---in the Google Ads UI. See the [`CartData`](https://developers.google.com/google-ads/api/reference/rpc/v26/CartData) object definition
    for the required structure, and learn more in
    [About conversions with cart data](https://support.google.com/google-ads/answer/9028254). Google Ads
    does not support `cart_data` when `wbraid` or `gbraid` is used without a
    `gclid`.

`user_identifiers`

:   When using enhanced conversions for leads, you *must* populate the
    [`user_identifiers`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion#user_identifiers) field with normalized and hashed
    user-provided data. If you have multiple user identifiers available, create a
    separate `UserIdentifier` for each one, up to five identifiers.

    > [!IMPORTANT]
    > **Important:** You can, and should, send *all* relevant data for a given conversion, even if you don't have a GCLID for it. Conversions that include only user-provided data are still useful and can contribute positively to campaign optimization.

`conversion_date_time`

:   The date and time of the conversion.

    The value must have a timezone specified, and the format must be

    `yyyy-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. However, if you plan on comparing your imported conversion
    data with those in the Google Ads UI, we recommend using the same timezone as your
    Google Ads account so that the conversion counts match. You can find more details
    and examples in [Prepare the data for import](https://support.google.com/google-ads/answer/7014069#prepare_data) and check
    [Codes and formats](https://developers.google.com/google-ads/api/data/codes-formats#timezone_ids) for a list of valid timezone IDs.

    > [!NOTE]
    > **Note:** If you're importing conversions for [Commissions (pay-per-conversion) for Hotel ads](https://support.google.com/google-ads/answer/9243945), you can only import conversions for clicks that occurred in the same month. Conversions imported after 4 AM Pacific Time (PT) on the first day of the following month of when the click happened are ignored.

`conversion_action`

:   The resource name of the [`ConversionAction`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionAction) for the
    offline conversion.

    The conversion action must have a `type` of `UPLOAD_CLICKS`, and must exist in
    the Google Ads conversion customer of the Google Ads account associated with the
    click.

`conversion_value`

:   The value of the conversion.

`currency_code`

:   The [currency code](https://developers.google.com/google-ads/api/data/codes-formats#currency_codes) of the `conversion_value`.

`consent`

:   It's **highly** recommended that you populate the [`consent`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion#consent) field
    of the `ClickConversion` object. If not set, it's possible that your
    conversions won't be attributable.

`order_id`

:   Also known as the transaction ID for the conversion. This field is optional,
    but strongly recommended, because it makes it simpler to reference imported
    conversions when making [adjustments](https://developers.google.com/google-ads/api/docs/conversions/upload-adjustments). If you set it during the
    import, you must use it for any adjustments. To learn more about how to use a
    transaction ID to minimize duplicate conversions, see
    [Use a transaction ID to minimize duplicate conversions](https://support.google.com/google-ads/answer/6386790).

`custom_variables`

:   The values for [custom conversion variables](https://developers.google.com/google-ads/api/docs/conversions/conversion-custom-variables).
    Google Ads does not support custom conversion variables when `wbraid` or `gbraid`
    is used without a `gclid`.

    > [!WARNING]
    > **Warning:** Do not include the `custom_variables` field when importing enhanced conversions for leads. Google Ads does not support custom variables with user-provided data in conversion imports. If you include custom variables, Google Ads ignores the user-provided data.

`conversion_environment`

:   Indicates the environment where this conversion was recorded. For example,
    `APP` or `WEB`.

> [!WARNING]
> **Warning:** Use of the `session_attributes_encoded` and `session_attributes_key_value_pairs` fields in the Google Ads API is only available to users on the allowlist. To include these fields in your conversion imports, upgrade to the [Data Manager API](https://developers.google.com/data-manager/api/devguides/events/google-ads/offline/send-events#session_attributes).

`session_attributes_encoded` and `session_attributes_key_value_pairs`

:   Session attributes represent aggregated identifiers used for conversion
    attribution. These work in addition to GCLIDs and URL parameters (such as
    GBRAIDs), and user-provided data, which is central to enhanced conversions for
    leads. There are two ways to import session attributes: by providing the
    encoded token generated by our JavaScript code in the browser, or by providing
    individual key-value pairs for each of the identifiers.

    To maximize your campaign performance, we recommend importing click
    identifiers, user-provided data, and session attributes with all of your
    conversions, if possible.


### Java

```java
// Sets one of the sessionAttributesEncoded or sessionAttributesKeyValuePairs if either is
// provided. The session attribute fields are only available to allowlisted users.
// To include these fields in conversion imports, upgrade to the Data Manager API.
if (rawRecord.containsKey("sessionAttributesEncoded")) {
  clickConversionBuilder.setSessionAttributesEncoded(
      ByteString.copyFromUtf8(rawRecord.get("sessionAttributesEncoded")));
} else if (rawRecord.containsKey("sessionAttributesMap")) {
  List<String> pairings =
      Arrays.stream(rawRecord.get("sessionAttributesMap").split(" "))
          .map(String::trim)
          .collect(Collectors.toList());
  SessionAttributesKeyValuePairs.Builder sessionAttributePairs =
      SessionAttributesKeyValuePairs.newBuilder();
  for (String pair : pairings) {
    String[] parts = pair.split("=", 2);
    if (parts.length != 2) {
      throw new IllegalArgumentException(
          "Failed to read the sessionAttributesMap. SessionAttributesMap must use a"
              + " space-delimited list of session attribute key value pairs. Each pair should"
              + " be separated by an equal sign, for example: 'gad_campaignid=12345"
              + " gad_source=1'");
    }
    sessionAttributePairs.addKeyValuePairs(
        SessionAttributeKeyValuePair.newBuilder()
            .setSessionAttributeKey(parts[0])
            .setSessionAttributeValue(parts[1])
            .build());
  }
  clickConversionBuilder.setSessionAttributesKeyValuePairs(sessionAttributePairs.build());
}
      
```

### C#

```c#
// The session attribute fields are only available to allowlisted users. To
// include these fields in conversion imports, upgrade to the Data Manager API.
if (!string.IsNullOrEmpty(sessionAttributesEncoded))
{
    clickConversion.SessionAttributesEncoded =
        ByteString.CopyFrom(sessionAttributesEncoded, Encoding.Unicode);
}
else if (!string.IsNullOrEmpty(sessionAttributes))
{
    IEnumerable<SessionAttributeKeyValuePair> parsedSessionAttributes =
        sessionAttributes.Split(';').Select(pair => {
            string[] split = pair.Split('=');
            return new SessionAttributeKeyValuePair()
            {
                SessionAttributeKey = split[0],
                SessionAttributeValue = split[1]
            };
        });

    clickConversion.SessionAttributesKeyValuePairs =
        new SessionAttributesKeyValuePairs();
    clickConversion.SessionAttributesKeyValuePairs.KeyValuePairs
        .AddRange(parsedSessionAttributes);
}
      
```

### PHP

```php
// Set one of the sessionAttributesEncoded or
// SessionAttributeKeyValuePair fields if either are provided. The session attribute
// fields are only available to allowlisted users. To include these fields in conversion
// imports, upgrade to the Data Manager API.
if (!empty($sessionAttributesEncoded)) {
    $clickConversion->setSessionAttributesEncoded($sessionAttributesEncoded);
} elseif (!empty($sessionAttributesDict)) {
    // Create a new container object to hold key-value pairs.
    $sessionAttributesKeyValuePairs = new SessionAttributesKeyValuePairs();
    // Initialize an array to hold individual key-value pair messages.
    $keyValuePairs = [];
    // Append each key-value pair provided to the $keyValuePairs array
    foreach ($sessionAttributesDict as $key => $value) {
        $pair = new SessionAttributeKeyValuePair();
        $pair->setSessionAttributeKey($key);
        $pair->setSessionAttributeValue($value);
        $keyValuePairs[] = $pair;
    }
    // Set the the full list of key-value pairs on the container object.
    $sessionAttributesKeyValuePairs->setKeyValuePairs($keyValuePairs);
    // Attach the container of key-value pairs to the ClickConversion object.
    $clickConversion->setSessionAttributesKeyValuePairs($sessionAttributesKeyValuePairs);
}
      
```

### Python

```python
# Set one of the session_attributes_encoded or
# session_attributes_key_value_pairs fields if either are provided.
# The session attribute fields are only available to allowlisted users.
# To include these fields in conversion imports, upgrade to the Data Manager API.
if session_attributes_encoded:
    click_conversion.session_attributes_encoded = session_attributes_encoded
elif session_attributes_dict:
    for key, value in session_attributes_dict.items():
        pair: SessionAttributeKeyValuePair = client.get_type(
            "SessionAttributeKeyValuePair"
        )
        pair.session_attribute_key = key
        pair.session_attribute_value = value
        click_conversion.session_attributes_key_value_pairs.key_value_pairs.append(
            pair
        )
      
```

### Ruby

```ruby
# Set one of the session_attributes_encoded or
# session_attributes_key_value_pairs fields if either are provided.
# The session attribute fields are only available to allowlisted users.
# To include these fields in conversion imports, upgrade to the Data Manager API.
if session_attributes_encoded != nil
  cc.class.module_eval { attr_accessor :session_attributes_encoded}
  cc.session_attributes_encoded = session_attributes_encoded
elsif session_attributes_hash != nil
  # Add new attribute to click conversion object
  cc.class.module_eval { attr_accessor :session_attributes_key_value_pairs}
  cc.session_attributes_key_value_pairs = ::Google::Ads::GoogleAds::V19::Services::SessionAttributesKeyValuePairs.new

  # Loop thru inputted session_attributes_hash to populate session_attributes_key_value_pairs
  session_attributes_hash.each do |key, value|
    pair = ::Google::Ads::GoogleAds::V19::Services::SessionAttributeKeyValuePair.new
    pair.session_attribute_key = key
    pair.session_attribute_value = value
    cc.session_attributes_key_value_pairs.key_value_pairs << pair
  end
end    
      
```

### Perl

```perl
# Set one of the session_attributes_encoded or session_attributes_key_value_pairs
# fields if either are provided.
if (defined $session_attributes_encoded) {
  $click_conversion->{sessionAttributesEncoded} = $session_attributes_encoded;
} elsif (defined $session_attributes_hash) {
  while (my ($key, $value) = each %$session_attributes_hash) {
    my $pair =
      Google::Ads::GoogleAds::V26::Services::ConversionUploadService::SessionAttributeKeyValuePair
      ->new({sessionAttributeKey => $key, sessionAttributeValue => $value});
    push @{$click_conversion->{sessionAttributesKeyValuePairs}{keyValuePairs}
    }, $pair;
  }
}
      
```

### curl

> [!NOTE]
> **Note:** While a direct REST code sample for this step isn't provided here, you can achieve this using a manual REST request.   
>
> Refer to the Google Ads API REST interface documentation and the method-specific reference pages. You will need to construct the JSON payload based on the proto definitions.   
>
> Key Resources:
>
> - [Using REST](https://developers.google.com/google-ads/api/rest/overview)
> - [REST Interface Structure](https://developers.google.com/google-ads/api/rest/design/overview)
> - [JSON Field Mappings](https://developers.google.com/google-ads/api/rest/design/json-mappings)
> - Consult the [REST API Reference](https://developers.google.com/google-ads/api/reference/rpc/latest/overview) for the specific service and method.

<br />

> [!WARNING]
> **Warning:** Use of the `user_ip_address` field in the Google Ads API is only available to users on the allowlist. To include these fields in your conversion imports, upgrade to the [Data Manager API](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event).

`user_ip_address`

:   The IP address of the customer when they arrived on the landing page after an
    ad click and before a conversion event. This is the IP address of the
    customer's device, not the advertiser's server.

    This field is a string representing an IP address in either [IPv4](https://en.wikipedia.org/wiki/IPv4) or
    [IPv6](https://en.wikipedia.org/wiki/IPv6) format. For example:

    - IPv4: `"192.0.2.0"`
    - IPv6: `"2001:0DB8:1234:5678:9999:1111:0000:0001"`

> [!NOTE]
> **Note:** Google Ads does not support IP address matching for end users in the European Economic Area (EEA), United Kingdom (UK), or Switzerland (CH). Add logic to conditionally exclude sharing IP addresses from users from these regions and ensure that you provide users with clear and comprehensive information about the data you collect on your sites, apps, and other properties and get consent where required by law or any applicable Google policies. See the [About offline conversion imports](https://support.google.com/google-ads/answer/2998031) page for more details.

#### Code example

This example shows how to set your normalized and hashed user-provided data onto
a `ClickConversion` object.


### Java

```java
// Creates an empty builder for constructing the click conversion.
ClickConversion.Builder clickConversionBuilder = ClickConversion.newBuilder();

// Extracts user email and phone from the raw data, normalizes and hashes it, then wraps it in
// UserIdentifier objects.
// Creates a separate UserIdentifier object for each. The data in this example is hardcoded, but
// in your application you might read the raw data from an input file.

// IMPORTANT: Since the identifier attribute of UserIdentifier
// (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) is a
// oneof
// (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set only ONE of
// hashedEmail, hashedPhoneNumber, mobileId, thirdPartyUserId, or addressInfo. Setting more
// than one of these attributes on the same UserIdentifier will clear all the other members
// of the oneof. For example, the following code is INCORRECT and will result in a
// UserIdentifier with ONLY a hashedPhoneNumber.
//
// UserIdentifier incorrectlyPopulatedUserIdentifier =
//     UserIdentifier.newBuilder()
//         .setHashedEmail("...")
//         .setHashedPhoneNumber("...")
//         .build();

ImmutableMap.Builder<String, String> rawRecordBuilder =
    ImmutableMap.<String, String>builder()
        .put("email", "alex.2+myalias@gmail.com")
        // Phone number to be converted to E.164 format, with a leading '+' as required.
        .put("phone", "+1 (800) 555-0102")
        // This example lets you put conversion details as arguments, but in reality you might
        // store this data alongside other user data, so we include it in this sample user
        // record.
        .put("conversionActionId", Long.toString(conversionActionId))
        .put("conversionDateTime", conversionDateTime)
        .put("conversionValue", Double.toString(conversionValue))
        .put("currencyCode", "USD");

// Adds entries for the optional fields.
if (orderId != null) {
  rawRecordBuilder.put("orderId", orderId);
}
if (gclid != null) {
  rawRecordBuilder.put("gclid", gclid);
}
if (adUserDataConsent != null) {
  rawRecordBuilder.put("adUserDataConsent", adUserDataConsent.name());
}
if (sessionAttributesEncoded != null) {
  rawRecordBuilder.put("sessionAttributesEncoded", sessionAttributesEncoded);
}
if (sessionAttributesMap != null) {
  rawRecordBuilder.put("sessionAttributesMap", sessionAttributesMap);
}

// Builds the map representing the record.
Map<String, String> rawRecord = rawRecordBuilder.build();

// 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 list for the user identifiers.
List<UserIdentifier> userIdentifiers = new ArrayList<>();

// Creates a user identifier using the hashed email address, using the normalize and hash method
// specifically for email addresses.
UserIdentifier emailIdentifier =
    UserIdentifier.newBuilder()
        // Optional: specify the user identifier source.
        .setUserIdentifierSource(UserIdentifierSource.FIRST_PARTY)
        // Uses the normalize and hash method specifically for email addresses.
        .setHashedEmail(normalizeAndHashEmailAddress(sha256Digest, rawRecord.get("email")))
        .build();
userIdentifiers.add(emailIdentifier);

// Creates a user identifier using normalized and hashed phone info.
UserIdentifier hashedPhoneNumberIdentifier =
    UserIdentifier.newBuilder()
        .setHashedPhoneNumber(normalizeAndHashPhoneNumber(sha256Digest, rawRecord.get("phone")))
        .build();
// Adds the hashed phone number identifier to the UserData object's list.
userIdentifiers.add(hashedPhoneNumberIdentifier);

// Adds the user identifiers to the conversion.
clickConversionBuilder.addAllUserIdentifiers(userIdentifiers);
      
```

### C#

```c#
// Adds a user identifier using the hashed email address, using the normalize
// and hash method specifically for email addresses.
clickConversion.UserIdentifiers.Add(new UserIdentifier()
{
    HashedEmail = NormalizeAndHashEmailAddress("alex.2@example.com"),
    // Optional: Specifies the user identifier source.
    UserIdentifierSource = UserIdentifierSource.FirstParty
});

// Adds a user identifier using normalized and hashed phone info.
clickConversion.UserIdentifiers.Add(new UserIdentifier()
{
    HashedPhoneNumber = NormalizeAndHash("+1 800 5550102"),
    // Optional: Specifies the user identifier source.
    UserIdentifierSource = UserIdentifierSource.FirstParty
});

// Adds a user identifier with all the required mailing address elements.
clickConversion.UserIdentifiers.Add(new UserIdentifier()
{
    AddressInfo = new OfflineUserAddressInfo()
    {
        // FirstName and LastName must be normalized and hashed.
        HashedFirstName = NormalizeAndHash("Alex"),
        HashedLastName = NormalizeAndHash("Quinn"),
        // CountryCode and PostalCode are sent in plain text.
        CountryCode = "US",
        PostalCode = "94045"
    }
});
      
```

### PHP

```php
// Creates a click conversion with the specified attributes.
$clickConversion = new ClickConversion();

// Extract user email and phone from the raw data, normalize and hash it, then wrap it in
// UserIdentifier objects. Creates a separate UserIdentifier object for each.
// The data in this example is hardcoded, but in your application you might read the raw
// data from an input file.

// IMPORTANT: Since the identifier attribute of UserIdentifier
// (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) is a
// oneof
// (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set only ONE
// of hashedEmail, hashedPhoneNumber, mobileId, thirdPartyUserId, or addressInfo. Setting
// more than one of these attributes on the same UserIdentifier will clear all the other
// members of the oneof. For example, the following code is INCORRECT and will result in a
// UserIdentifier with ONLY a hashedPhoneNumber.
//
// $incorrectlyPopulatedUserIdentifier = new UserIdentifier([
//    'hashed_email' => '...',
//    'hashed_phone_number' => '...'
// ]);

$rawRecord = [
    // Email address that includes a period (.) before the Gmail domain.
    'email' => 'alex.2@example.com',
    // Phone number to be converted to E.164 format, with a leading '+' as required.
    'phone' => '+1 800 5550102',
    // This example lets you input conversion details as arguments, but in reality you might
    // store this data alongside other user data, so we include it in this sample user
    // record.
    'orderId' => $orderId,
    'gclid' => $gclid,
    'conversionActionId' => $conversionActionId,
    'conversionDateTime' => $conversionDateTime,
    'conversionValue' => $conversionValue,
    'currencyCode' => 'USD',
    'adUserDataConsent' => $adUserDataConsent,
    'sessionAttributesEncoded' => $sessionAttributesEncoded,
    'sessionAttributesDict' => $sessionAttributesDict
];

// Creates a list for the user identifiers.
$userIdentifiers = [];

// 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 using the hashed email address, using the normalize and hash
// method specifically for email addresses.
$emailIdentifier = new UserIdentifier(
    [
    // Uses the normalize and hash method specifically for email addresses.
    'hashed_email' => self::normalizeAndHashEmailAddress(
        $hashAlgorithm,
        $rawRecord['email']
    ),
    // Optional: Specifies the user identifier source.
    'user_identifier_source' => UserIdentifierSource::FIRST_PARTY
    ]
);
$userIdentifiers[] = $emailIdentifier;

// Checks if the record has a phone number, and if so, adds a UserIdentifier for it.
if (array_key_exists('phone', $rawRecord)) {
    $hashedPhoneNumberIdentifier = new UserIdentifier(
        [
        'hashed_phone_number' => self::normalizeAndHash(
            $hashAlgorithm,
            $rawRecord['phone'],
            true
        )
        ]
    );
    // Adds the hashed email identifier to the user identifiers list.
    $userIdentifiers[] = $hashedPhoneNumberIdentifier;
}

// Adds the user identifiers to the conversion.
$clickConversion->setUserIdentifiers($userIdentifiers);
      
```

### Python

```python
# Extract user email and phone from the raw data, normalize and hash it,
# then wrap it in UserIdentifier objects. Create a separate UserIdentifier
# object for each. The data in this example is hardcoded, but in your
# application you might read the raw data from an input file.

# IMPORTANT: Since the identifier attribute of UserIdentifier
# (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier)
# is a oneof
# (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must
# set only ONE of hashed_email, hashed_phone_number, mobile_id,
# third_party_user_id, or address_info. Setting more than one of these
# attributes on the same UserIdentifier will clear all the other members of
# the oneof. For example, the following code is INCORRECT and will result in
# a UserIdentifier with ONLY a hashed_phone_number:
#
# incorrectly_populated_user_identifier = client.get_type("UserIdentifier")
# incorrectly_populated_user_identifier.hashed_email = "...""
# incorrectly_populated_user_identifier.hashed_phone_number = "...""

raw_record: Dict[str, Union[str, float]] = {
    # Email address that includes a period (.) before the Gmail domain.
    "email": "alex.2@example.com",
    # Phone number to be converted to E.164 format, with a leading '+' as
    # required.
    "phone": "+1 800 5550102",
    # This example lets you input conversion details as arguments,
    # but in reality you might store this data alongside other user data,
    # so we include it in this sample user record.
    "order_id": order_id,
    "gclid": gclid,
    "conversion_action_id": conversion_action_id,
    "conversion_date_time": conversion_date_time,
    "conversion_value": conversion_value,
    "currency_code": "USD",
    "ad_user_data_consent": ad_user_data_consent,
}

# Constructs the click conversion.
click_conversion: ClickConversion = client.get_type("ClickConversion")
# Creates a user identifier using the hashed email address, using the
# normalize and hash method specifically for email addresses.
email_identifier: UserIdentifier = client.get_type("UserIdentifier")
# Optional: Specifies the user identifier source.
email_identifier.user_identifier_source = (
    client.enums.UserIdentifierSourceEnum.FIRST_PARTY
)
# Uses the normalize and hash method specifically for email addresses.
email_identifier.hashed_email = normalize_and_hash_email_address(
    raw_record["email"]
)
# Adds the user identifier to the conversion.
click_conversion.user_identifiers.append(email_identifier)

# Checks if the record has a phone number, and if so, adds a UserIdentifier
# for it.
if raw_record.get("phone") is not None:
    phone_identifier: UserIdentifier = client.get_type("UserIdentifier")
    phone_identifier.hashed_phone_number = normalize_and_hash(
        raw_record["phone"]
    )
    # Adds the phone identifier to the conversion adjustment.
    click_conversion.user_identifiers.append(phone_identifier)
      
```

### Ruby

```ruby
# Extract user email and phone from the raw data, normalize and hash it,
# then wrap it in UserIdentifier objects. Create a separate UserIdentifier
# object for each. The data in this example is hardcoded, but in your
# application you might read the raw data from an input file.

# IMPORTANT: Since the identifier attribute of UserIdentifier
# (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier)
# is a oneof
# (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must
# set only ONE of hashed_email, hashed_phone_number, mobile_id,
# third_party_user_id, or address_info. Setting more than one of these
# attributes on the same UserIdentifier will clear all the other members of
# the oneof. For example, the following code is INCORRECT and will result in
# a UserIdentifier with ONLY a hashed_phone_number:
#
# incorrectly_populated_user_identifier.hashed_email = "...""
# incorrectly_populated_user_identifier.hashed_phone_number = "...""

raw_record = {
  # Email address that includes a period (.) before the Gmail domain.
  "email" => "alex.2@example.com",
  # Phone number to be converted to E.164 format, with a leading '+' as
  # required.
  "phone" => "+1 800 5550102",
  # This example lets you input conversion details as arguments,
  # but in reality you might store this data alongside other user data,
  # so we include it in this sample user record.
  "order_id" => order_id,
  "gclid" => gclid,
  "conversion_action_id" => conversion_action_id,
  "conversion_date_time" => conversion_date_time,
  "conversion_value" => conversion_value,
  "currency_code" => "USD",
  "ad_user_data_consent" => ad_user_data_consent,
  "session_attributes_encoded" => session_attributes_encoded,
  "session_attributes_hash" => session_attributes_hash
}

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

  unless raw_record["gclid"].nil?
    cc.gclid = gclid
  end

  # 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 raw_record["ad_user_data_consent"].nil?
    cc.consent = client.resource.consent do |c|
      c.ad_user_data = ad_user_data_consent
    end
  end

  # Set one of the session_attributes_encoded or
  # session_attributes_key_value_pairs fields if either are provided.
  # The session attribute fields are only available to allowlisted users.
  # To include these fields in conversion imports, upgrade to the Data Manager API.
  if session_attributes_encoded != nil
    cc.class.module_eval { attr_accessor :session_attributes_encoded}
    cc.session_attributes_encoded = session_attributes_encoded
  elsif session_attributes_hash != nil
    # Add new attribute to click conversion object
    cc.class.module_eval { attr_accessor :session_attributes_key_value_pairs}
    cc.session_attributes_key_value_pairs = ::Google::Ads::GoogleAds::V19::Services::SessionAttributesKeyValuePairs.new

    # Loop thru inputted session_attributes_hash to populate session_attributes_key_value_pairs
    session_attributes_hash.each do |key, value|
      pair = ::Google::Ads::GoogleAds::V19::Services::SessionAttributeKeyValuePair.new
      pair.session_attribute_key = key
      pair.session_attribute_value = value
      cc.session_attributes_key_value_pairs.key_value_pairs << pair
    end
  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 |ui|
    ui.hashed_email = normalize_and_hash_email(raw_record["email"])
    # Optional: Specifies the user identifier source.
    ui.user_identifier_source = :FIRST_PARTY
  end

  # Checks if the record has a phone number, and if so, adds a UserIdentifier
  # for it.
  unless raw_record["phone"].nil?
    cc.user_identifiers << client.resource.user_identifier do |ui|
      ui.hashed_phone_number = normalize_and_hash(raw_record["phone"])
    end
  end
end
      
```

### Perl

```perl
# Create an empty click conversion.
my $click_conversion =
  Google::Ads::GoogleAds::V26::Services::ConversionUploadService::ClickConversion
  ->new({});

# Extract user email and phone from the raw data, normalize and hash it,
# then wrap it in UserIdentifier objects. Create a separate UserIdentifier
# object for each.
# The data in this example is hardcoded, but in your application
# you might read the raw data from an input file.
#
# IMPORTANT: Since the identifier attribute of UserIdentifier
# (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier)
# is a oneof
# (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set
# only ONE of hashed_email, hashed_phone_number, mobile_id, third_party_user_id,
# or address-info. Setting more than one of these attributes on the same UserIdentifier
# will clear all the other members of the oneof. For example, the following code is
# INCORRECT and will result in a UserIdentifier with ONLY a hashed_phone_number:
#
# my $incorrect_user_identifier = Google::Ads::GoogleAds::V26::Common::UserIdentifier->new({
#   hashedEmail => '...',
#   hashedPhoneNumber => '...',
# });
my $raw_record = {
  # Email address that includes a period (.) before the Gmail domain.
  email => 'alex.2@example.com',
  # Phone number to be converted to E.164 format, with a leading '+' as
  # required.
  phone => '+1 800 5550102',
  # This example lets you input conversion details as arguments,
  # but in reality you might store this data alongside other user data,
  # so we include it in this sample user record.
  orderId            => $order_id,
  gclid              => $gclid,
  conversionActionId => $conversion_action_id,
  conversionDateTime => $conversion_date_time,
  conversionValue    => $conversion_value,
  currencyCode       => "USD",
  adUserDataConsent  => $ad_user_data_consent
};
my $user_identifiers = [];

# Create a user identifier using the hashed email address, using the normalize
# and hash method specifically for email addresses.
my $hashed_email = normalize_and_hash_email_address($raw_record->{email});
push(
  @$user_identifiers,
  Google::Ads::GoogleAds::V26::Common::UserIdentifier->new({
      hashedEmail => $hashed_email,
      # Optional: Specify the user identifier source.
      userIdentifierSource => FIRST_PARTY
    }));

# Create a user identifier using normalized and hashed phone info.
my $hashed_phone = normalize_and_hash($raw_record->{phone});
push(
  @$user_identifiers,
  Google::Ads::GoogleAds::V26::Common::UserIdentifier->new({
      hashedPhone => $hashed_phone,
      # Optional: Specify the user identifier source.
      userIdentifierSource => FIRST_PARTY
    }));

# Add the user identifiers to the conversion.
$click_conversion->{userIdentifiers} = $user_identifiers;
      
```

### curl

> [!NOTE]
> **Note:** While a direct REST code sample for this step isn't provided here, you can achieve this using a manual REST request.   
>
> Refer to the Google Ads API REST interface documentation and the method-specific reference pages. You will need to construct the JSON payload based on the proto definitions.   
>
> Key Resources:
>
> - [Using REST](https://developers.google.com/google-ads/api/rest/overview)
> - [REST Interface Structure](https://developers.google.com/google-ads/api/rest/design/overview)
> - [JSON Field Mappings](https://developers.google.com/google-ads/api/rest/design/json-mappings)
> - Consult the [REST API Reference](https://developers.google.com/google-ads/api/reference/rpc/latest/overview) for the specific service and method.

<br />

This example shows how to set other necessary fields on a `ClickConversion`
object.


### Java

```java
// Adds details of the conversion.
clickConversionBuilder.setConversionAction(
    ResourceNames.conversionAction(
        customerId, Long.parseLong(rawRecord.get("conversionActionId"))));
clickConversionBuilder.setConversionDateTime(rawRecord.get("conversionDateTime"));
clickConversionBuilder.setConversionValue(Double.parseDouble(rawRecord.get("conversionValue")));
clickConversionBuilder.setCurrencyCode(rawRecord.get("currencyCode"));

// Sets the order ID if provided.
if (rawRecord.containsKey("orderId")) {
  clickConversionBuilder.setOrderId(rawRecord.get("orderId"));
}

// Sets the Google click ID (gclid) if provided.
if (rawRecord.containsKey("gclid")) {
  clickConversionBuilder.setGclid(rawRecord.get("gclid"));
}

// Sets the consent information, if provided.
if (rawRecord.containsKey("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.
  clickConversionBuilder.setConsent(
      Consent.newBuilder()
          .setAdUserData(ConsentStatus.valueOf(rawRecord.get("adUserDataConsent"))));
}

// Sets one of the sessionAttributesEncoded or sessionAttributesKeyValuePairs if either is
// provided. The session attribute fields are only available to allowlisted users.
// To include these fields in conversion imports, upgrade to the Data Manager API.
if (rawRecord.containsKey("sessionAttributesEncoded")) {
  clickConversionBuilder.setSessionAttributesEncoded(
      ByteString.copyFromUtf8(rawRecord.get("sessionAttributesEncoded")));
} else if (rawRecord.containsKey("sessionAttributesMap")) {
  List<String> pairings =
      Arrays.stream(rawRecord.get("sessionAttributesMap").split(" "))
          .map(String::trim)
          .collect(Collectors.toList());
  SessionAttributesKeyValuePairs.Builder sessionAttributePairs =
      SessionAttributesKeyValuePairs.newBuilder();
  for (String pair : pairings) {
    String[] parts = pair.split("=", 2);
    if (parts.length != 2) {
      throw new IllegalArgumentException(
          "Failed to read the sessionAttributesMap. SessionAttributesMap must use a"
              + " space-delimited list of session attribute key value pairs. Each pair should"
              + " be separated by an equal sign, for example: 'gad_campaignid=12345"
              + " gad_source=1'");
    }
    sessionAttributePairs.addKeyValuePairs(
        SessionAttributeKeyValuePair.newBuilder()
            .setSessionAttributeKey(parts[0])
            .setSessionAttributeValue(parts[1])
            .build());
  }
  clickConversionBuilder.setSessionAttributesKeyValuePairs(sessionAttributePairs.build());
}

// Calls build to build the conversion.
ClickConversion clickConversion = clickConversionBuilder.build();
      
```

### C#

```c#
// Adds details of the conversion.
clickConversion.ConversionAction =
    ResourceNames.ConversionAction(customerId, conversionActionId);
clickConversion.ConversionDateTime = conversionDateTime;
clickConversion.ConversionValue = conversionValue;
clickConversion.CurrencyCode = "USD";

// Sets the order ID if provided.
if (!string.IsNullOrEmpty(orderId))
{
    clickConversion.OrderId = orderId;
}

// Sets the Google click ID (gclid) if provided.
if (!string.IsNullOrEmpty(gclid))
{
    clickConversion.Gclid = gclid;
}

// The session attribute fields are only available to allowlisted users. To
// include these fields in conversion imports, upgrade to the Data Manager API.
if (!string.IsNullOrEmpty(sessionAttributesEncoded))
{
    clickConversion.SessionAttributesEncoded =
        ByteString.CopyFrom(sessionAttributesEncoded, Encoding.Unicode);
}
else if (!string.IsNullOrEmpty(sessionAttributes))
{
    IEnumerable<SessionAttributeKeyValuePair> parsedSessionAttributes =
        sessionAttributes.Split(';').Select(pair => {
            string[] split = pair.Split('=');
            return new SessionAttributeKeyValuePair()
            {
                SessionAttributeKey = split[0],
                SessionAttributeValue = split[1]
            };
        });

    clickConversion.SessionAttributesKeyValuePairs =
        new SessionAttributesKeyValuePairs();
    clickConversion.SessionAttributesKeyValuePairs.KeyValuePairs
        .AddRange(parsedSessionAttributes);
}

      
```

### PHP

```php
// Adds details of the conversion.
$clickConversion->setConversionAction(
    ResourceNames::forConversionAction($customerId, $rawRecord['conversionActionId'])
);
$clickConversion->setConversionDateTime($rawRecord['conversionDateTime']);
$clickConversion->setConversionValue($rawRecord['conversionValue']);
$clickConversion->setCurrencyCode($rawRecord['currencyCode']);

// Sets the order ID if provided.
if (!empty($rawRecord['orderId'])) {
    $clickConversion->setOrderId($rawRecord['orderId']);
}

// Sets the Google click ID (gclid) if provided.
if (!empty($rawRecord['gclid'])) {
    $clickConversion->setGclid($rawRecord['gclid']);
}

// Sets the ad user data consent if provided.
if (!empty($rawRecord['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.
    $clickConversion->setConsent(
        new Consent(['ad_user_data' => $rawRecord['adUserDataConsent']])
    );
}

// Set one of the sessionAttributesEncoded or
// SessionAttributeKeyValuePair fields if either are provided. The session attribute
// fields are only available to allowlisted users. To include these fields in conversion
// imports, upgrade to the Data Manager API.
if (!empty($sessionAttributesEncoded)) {
    $clickConversion->setSessionAttributesEncoded($sessionAttributesEncoded);
} elseif (!empty($sessionAttributesDict)) {
    // Create a new container object to hold key-value pairs.
    $sessionAttributesKeyValuePairs = new SessionAttributesKeyValuePairs();
    // Initialize an array to hold individual key-value pair messages.
    $keyValuePairs = [];
    // Append each key-value pair provided to the $keyValuePairs array
    foreach ($sessionAttributesDict as $key => $value) {
        $pair = new SessionAttributeKeyValuePair();
        $pair->setSessionAttributeKey($key);
        $pair->setSessionAttributeValue($value);
        $keyValuePairs[] = $pair;
    }
    // Set the the full list of key-value pairs on the container object.
    $sessionAttributesKeyValuePairs->setKeyValuePairs($keyValuePairs);
    // Attach the container of key-value pairs to the ClickConversion object.
    $clickConversion->setSessionAttributesKeyValuePairs($sessionAttributesKeyValuePairs);
}
      
```

### Python

```python
# Add details of the conversion.
# Gets the conversion action resource name.
conversion_action_service: ConversionActionServiceClient = (
    client.get_service("ConversionActionService")
)
click_conversion.conversion_action = (
    conversion_action_service.conversion_action_path(
        customer_id, raw_record["conversion_action_id"]
    )
)
click_conversion.conversion_date_time = raw_record["conversion_date_time"]
click_conversion.conversion_value = raw_record["conversion_value"]
click_conversion.currency_code = raw_record["currency_code"]

# Sets the order ID if provided.
if raw_record.get("order_id"):
    click_conversion.order_id = raw_record["order_id"]

# Sets the gclid if provided.
if raw_record.get("gclid"):
    click_conversion.gclid = raw_record["gclid"]

# 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 raw_record["ad_user_data_consent"]:
    click_conversion.consent.ad_user_data = client.enums.ConsentStatusEnum[
        raw_record["ad_user_data_consent"]
    ]

# Set one of the session_attributes_encoded or
# session_attributes_key_value_pairs fields if either are provided.
# The session attribute fields are only available to allowlisted users.
# To include these fields in conversion imports, upgrade to the Data Manager API.
if session_attributes_encoded:
    click_conversion.session_attributes_encoded = session_attributes_encoded
elif session_attributes_dict:
    for key, value in session_attributes_dict.items():
        pair: SessionAttributeKeyValuePair = client.get_type(
            "SessionAttributeKeyValuePair"
        )
        pair.session_attribute_key = key
        pair.session_attribute_value = value
        click_conversion.session_attributes_key_value_pairs.key_value_pairs.append(
            pair
        )
      
```

### Ruby

```ruby
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

unless raw_record["gclid"].nil?
  cc.gclid = gclid
end

# 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 raw_record["ad_user_data_consent"].nil?
  cc.consent = client.resource.consent do |c|
    c.ad_user_data = ad_user_data_consent
  end
end

# Set one of the session_attributes_encoded or
# session_attributes_key_value_pairs fields if either are provided.
# The session attribute fields are only available to allowlisted users.
# To include these fields in conversion imports, upgrade to the Data Manager API.
if session_attributes_encoded != nil
  cc.class.module_eval { attr_accessor :session_attributes_encoded}
  cc.session_attributes_encoded = session_attributes_encoded
elsif session_attributes_hash != nil
  # Add new attribute to click conversion object
  cc.class.module_eval { attr_accessor :session_attributes_key_value_pairs}
  cc.session_attributes_key_value_pairs = ::Google::Ads::GoogleAds::V19::Services::SessionAttributesKeyValuePairs.new

  # Loop thru inputted session_attributes_hash to populate session_attributes_key_value_pairs
  session_attributes_hash.each do |key, value|
    pair = ::Google::Ads::GoogleAds::V19::Services::SessionAttributeKeyValuePair.new
    pair.session_attribute_key = key
    pair.session_attribute_value = value
    cc.session_attributes_key_value_pairs.key_value_pairs << pair
  end
end    
      
```

### Perl

```perl
# Add details of the conversion.
$click_conversion->{conversionAction} =
  Google::Ads::GoogleAds::V26::Utils::ResourceNames::conversion_action(
  $customer_id, $raw_record->{conversionActionId});
$click_conversion->{conversionDateTime} = $raw_record->{conversionDateTime};
$click_conversion->{conversionValue}    = $raw_record->{conversionValue};
$click_conversion->{currencyCode}       = $raw_record->{currencyCode};

# Set the order ID if provided.
if (defined $raw_record->{orderId}) {
  $click_conversion->{orderId} = $raw_record->{orderId};
}

# Set the Google click ID (gclid) if provided.
if (defined $raw_record->{gclid}) {
  $click_conversion->{gclid} = $raw_record->{gclid};
}

# Set the consent information, if provided.
if (defined $raw_record->{adUserDataConsent}) {
  $click_conversion->{consent} =
    Google::Ads::GoogleAds::V26::Common::Consent->new({
      adUserData => $raw_record->{adUserDataConsent}});
}

# Set one of the session_attributes_encoded or session_attributes_key_value_pairs
# fields if either are provided.
if (defined $session_attributes_encoded) {
  $click_conversion->{sessionAttributesEncoded} = $session_attributes_encoded;
} elsif (defined $session_attributes_hash) {
  while (my ($key, $value) = each %$session_attributes_hash) {
    my $pair =
      Google::Ads::GoogleAds::V26::Services::ConversionUploadService::SessionAttributeKeyValuePair
      ->new({sessionAttributeKey => $key, sessionAttributeValue => $value});
    push @{$click_conversion->{sessionAttributesKeyValuePairs}{keyValuePairs}
    }, $pair;
  }
}
      
```

### curl

> [!NOTE]
> **Note:** While a direct REST code sample for this step isn't provided here, you can achieve this using a manual REST request.   
>
> Refer to the Google Ads API REST interface documentation and the method-specific reference pages. You will need to construct the JSON payload based on the proto definitions.   
>
> Key Resources:
>
> - [Using REST](https://developers.google.com/google-ads/api/rest/overview)
> - [REST Interface Structure](https://developers.google.com/google-ads/api/rest/design/overview)
> - [JSON Field Mappings](https://developers.google.com/google-ads/api/rest/design/json-mappings)
> - Consult the [REST API Reference](https://developers.google.com/google-ads/api/reference/rpc/latest/overview) for the specific service and method.

<br />

### Construct the request

Once your `ClickConversion` objects are configured and added to the
`conversions` field of the `UploadClickConversionsRequest` object, set the
following fields and pass the request to the `UploadClickConversions` method on
the `ConversionUploadService`.

`customer_id`
:   Set this to the **Google Ads conversion customer** of the account that is the
    source of the clicks. If you're unsure which account is the correct one, refer
    to the `customer.conversion_tracking_setting.google_ads_conversion_customer`
    field in the example query in the [getting started](https://developers.google.com/google-ads/api/docs/conversions/getting-started#conversion_tracking_setup) section.

`job_id`

:   Provides a mechanism for associating your import requests with the per-job
    information in [offline data diagnostics](https://developers.google.com/google-ads/api/docs/conversions/upload-summaries).

    If you don't set this field, the Google Ads API assigns each request a unique value
    in the range of `[2^31, 2^63)`. If you would prefer to group multiple requests
    into a single logical job, set this field to the same value in the range
    `[1, 2^31)` on every request in your job.

    The `job_id` in the [response](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsResponse) contains the job ID
    for the request, regardless of whether you specified a value or let the
    Google Ads API assign a value.

`partial_failure`

:   This field must be set to `true` when importing conversions. Follow the
    [partial failures guidelines](https://developers.google.com/google-ads/api/docs/best-practices/partial-failures) when processing the
    response.

### Import the request

Once you've populated your `ClickConversion` objects, and constructed your
request, you can submit your import.


### Java

```java
// Creates the conversion upload service client.
try (ConversionUploadServiceClient conversionUploadServiceClient =
    googleAdsClient.getLatestVersion().createConversionUploadServiceClient()) {
  // Uploads the click conversion. Partial failure should always be set to true.

  // NOTE: This request contains a single conversion as a demonstration.  However, if you have
  // multiple conversions to upload, it's best to upload multiple conversions per request
  // instead of sending a separate request per conversion. See the following for per-request
  // limits:
  // https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service
  UploadClickConversionsResponse response =
      conversionUploadServiceClient.uploadClickConversions(
          UploadClickConversionsRequest.newBuilder()
              .setCustomerId(Long.toString(customerId))
              .addConversions(clickConversion)
              // Enables partial failure (must be true).
              .setPartialFailure(true)
              .build());
      
```

### C#

```c#
// Uploads the click conversion. Partial failure should always be set to true.
// NOTE: This request contains a single conversion as a demonstration.
// However, if you have multiple conversions to upload, it's best to upload multiple
// conversions per request instead of sending a separate request per conversion.
// See the following for per-request limits:
// https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload
UploadClickConversionsResponse response =
    conversionUploadService.UploadClickConversions(
        new UploadClickConversionsRequest()
        {
            CustomerId = customerId.ToString(),
            Conversions = { clickConversion },
            // Enables partial failure (must be true).
            PartialFailure = true
        });

      
```

### PHP

```php
// Issues a request to upload the click conversion.
$conversionUploadServiceClient = $googleAdsClient->getConversionUploadServiceClient();
// NOTE: This request contains a single conversion as a demonstration.  However, if you have
// multiple conversions to upload, it's best to upload multiple conversions per request
// instead of sending a separate request per conversion. See the following for per-request
// limits:
// https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service
$response = $conversionUploadServiceClient->uploadClickConversions(
    // Enables partial failure (must be true).
    UploadClickConversionsRequest::build($customerId, [$clickConversion], true)
);
      
```

### Python

```python
# Creates the conversion upload service client.
conversion_upload_service: ConversionUploadServiceClient = (
    client.get_service("ConversionUploadService")
)
# Uploads the click conversion. Partial failure should always be set to
# True.
# NOTE: This request only uploads a single conversion, but if you have
# multiple conversions to upload, it's most efficient to upload them in a
# single request. See the following for per-request limits for reference:
# https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service
response: UploadClickConversionsResponse = (
    conversion_upload_service.upload_click_conversions(
        customer_id=customer_id,
        conversions=[click_conversion],
        # Enables partial failure (must be true).
        partial_failure=True,
    )
)
      
```

### Ruby

```ruby
response = client.service.conversion_upload.upload_click_conversions(
  customer_id: customer_id,
  conversions: [click_conversion],
  # Partial failure must be true.
  partial_failure: true,
)

if response.partial_failure_error
  puts "Partial failure encountered: #{response.partial_failure_error.message}"
else
  result = response.results.first
  puts "Uploaded click conversion that happened at #{result.conversion_date_time} " \
    "to #{result.conversion_action}."
end
      
```

### Perl

```perl
# Upload the click conversion. Partial failure should always be set to true.
#
# NOTE: This request contains a single conversion as a demonstration.
# However, if you have multiple conversions to upload, it's best to
# upload multiple conversions per request instead of sending a separate
# request per conversion. See the following for per-request limits:
# https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service
my $response =
  $api_client->ConversionUploadService()->upload_click_conversions({
    customerId  => $customer_id,
    conversions => [$click_conversion],
    # Enable partial failure (must be true).
    partialFailure => "true"
  });
      
```

### curl

> [!NOTE]
> **Note:** While a direct REST code sample for this step isn't provided here, you can achieve this using a manual REST request.   
>
> Refer to the Google Ads API REST interface documentation and the method-specific reference pages. You will need to construct the JSON payload based on the proto definitions.   
>
> Key Resources:
>
> - [Using REST](https://developers.google.com/google-ads/api/rest/overview)
> - [REST Interface Structure](https://developers.google.com/google-ads/api/rest/design/overview)
> - [JSON Field Mappings](https://developers.google.com/google-ads/api/rest/design/json-mappings)
> - Consult the [REST API Reference](https://developers.google.com/google-ads/api/reference/rpc/latest/overview) for the specific service and method.

<br />

#### REST request and response structure

When calling
[`ConversionUploadService.UploadClickConversions`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadService/UploadClickConversions) over REST,
send a `POST` request with `partialFailure` set to `true`:

    POST https://googleads.googleapis.com/v26/customers/{customer_id}:uploadClickConversions
    {
      "partialFailure": true,
      "jobId": 1001,
      "conversions": [
        {
          "conversionAction": "customers/{customer_id}/conversionActions/{conversion_action_id}",
          "conversionDateTime": "2026-06-01 14:30:00-04:00",
          "conversionValue": 125.50,
          "currencyCode": "USD",
          "orderId": "ORDER-2026-0001",
          "gclid": "EAIaIQobChMI...",
          "consent": {
            "adUserData": "GRANTED"
          },
          "userIdentifiers": [
            {
              "userIdentifierSource": "FIRST_PARTY",
              "hashedEmail": "f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a"
            },
            {
              "userIdentifierSource": "FIRST_PARTY",
              "hashedPhoneNumber": "42975f8f9e46c2e13c1a2294957e5cb8d8e6f4f7341a036c6e595bc9f5e24621"
            }
          ]
        },
        {
          "conversionAction": "customers/{customer_id}/conversionActions/{conversion_action_id}",
          "conversionDateTime": "2026-06-01 14:35:00-04:00",
          "conversionValue": 80.00,
          "currencyCode": "USD",
          "orderId": "ORDER-2026-0002",
          "gclid": "EAIaIQobChMI_expired...",
          "consent": {
            "adUserData": "GRANTED"
          }
        }
      ]
    }

In this two-conversion batch where the second item fails, successful rows return
populated [`ClickConversionResult`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversionResult) entries in `results[]`, while
failed rows return empty `{}` objects in `results[]` alongside error details in
`partialFailureError` on
[`UploadClickConversionsResponse`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsResponse):

    {
      "results": [
        {
          "conversionAction": "customers/{customer_id}/conversionActions/{conversion_action_id}",
          "conversionDateTime": "2026-06-01 14:30:00-04:00",
          "gclid": "EAIaIQobChMI...",
          "userIdentifiers": [
            {
              "userIdentifierSource": "FIRST_PARTY",
              "hashedEmail": "f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a"
            },
            {
              "userIdentifierSource": "FIRST_PARTY",
              "hashedPhoneNumber": "42975f8f9e46c2e13c1a2294957e5cb8d8e6f4f7341a036c6e595bc9f5e24621"
            }
          ]
        },
        {}
      ],
      "jobId": "1001",
      "partialFailureError": {
        "code": 3,
        "message": "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data., at conversions[1].gclid",
        "details": [
          {
            "@type": "type.googleapis.com/google.ads.googleads.v26.errors.GoogleAdsFailure",
            "errors": [
              {
                "errorCode": {
                  "conversionUploadError": "EXPIRED_EVENT"
                },
                "message": "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data.",
                "location": {
                  "fieldPathElements": [
                    {
                      "fieldName": "conversions",
                      "index": 1
                    },
                    {
                      "fieldName": "gclid"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    }

## Review your imports

Use the
[enhanced conversions for leads diagnostics report](https://support.google.com/google-ads/answer/15249267) to
review the overall health of your recent imports.

### Query upload diagnostics with GAQL

You can also inspect account-level upload health, recent job counts, and error
alerts programmatically by querying
[`offline_conversion_upload_client_summary`](https://developers.google.com/google-ads/api/docs/conversions/upload-summaries) against
the conversion customer ID:

    SELECT
      customer.id,
      offline_conversion_upload_client_summary.client,
      offline_conversion_upload_client_summary.status,
      offline_conversion_upload_client_summary.total_event_count,
      offline_conversion_upload_client_summary.successful_event_count,
      offline_conversion_upload_client_summary.success_rate,
      offline_conversion_upload_client_summary.pending_event_count,
      offline_conversion_upload_client_summary.pending_rate,
      offline_conversion_upload_client_summary.last_upload_date_time,
      offline_conversion_upload_client_summary.alerts,
      offline_conversion_upload_client_summary.daily_summaries,
      offline_conversion_upload_client_summary.job_summaries
    FROM offline_conversion_upload_client_summary

> [!IMPORTANT]
> **Important:** A successful import response from the API does not necessarily mean the conversions have been attributed. See [Data discrepancies after a successful upload](https://support.google.com/google-ads/answer/13321563#Datadiscrepanciesaftersuccessfulupload) for more details on data discrepancies after a successful import.

Imported conversions are reflected in reports for the
[**impression** date of the original click](https://support.google.com/google-ads/answer/13321563#Date_range_reporting_date&zippy=%2Cdifferences-in-date-range-and-reporting-date), not the date of
the import request or the date of the `conversion_date_time` of the
[`ClickConversion`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion).

It takes up to three hours for imported conversion statistics to appear in your
Google Ads account for last-click attribution. For other search attribution models,
it can take longer than three hours. Consult the
[data freshness guide](https://support.google.com/google-ads/answer/2544985) for more information.

When reporting on conversion metrics for your campaigns, refer to
[Mapping user interface metrics](https://developers.google.com/google-ads/api/docs/conversions/reporting#map-metrics) to correlate Google Ads UI metrics with
Google Ads API reporting fields. You can also query the
[`conversion_action`](https://developers.google.com/google-ads/api/fields/v26/conversion_action) resource to view the total number of
conversions and the total conversion value for a given conversion action.

## Best practices

Keep the following best practices in mind when implementing enhanced conversions
for leads.

### Send all conversion data regardless of completeness

To ensure full and accurate conversion reporting, import all available offline
conversion events, including those that might not have come from Google Ads.
Conversions that include only user-provided data are still useful and can
contribute positively to Google Ads campaign optimization.

If you assign an `order_id` to a conversion, we recommend including it. If you
have the GCLID for a conversion, we recommend sending it in addition to the
`user_identifiers` for improved performance. Furthermore, if you have more than
one `UserIdentifier` for the conversion, include all of them on the
`ClickConversion` object to improve the likelihood of a match.

### Batch multiple conversions in a single request

If you have multiple conversions to import, batch the conversions into one
[`UploadClickConversionsRequest`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsRequest), rather than
sending an import request per conversion.

Check the [quota guide](https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion-upload-service) for limits on the number of conversions per
request.

If you want [offline data diagnostics](https://developers.google.com/google-ads/api/docs/conversions/upload-summaries) to group a set
of requests under the same logical job, set the [`job_id`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsRequest#job_id) of all the
requests to the same value. This can be useful if you have a single job or
process that imports a large number of conversions using multiple requests. If
you set the `job_id` on each of those requests to the same value, then you can
retrieve a single entry for the job from [`job_summaries`](https://developers.google.com/google-ads/api/reference/rpc/v26/OfflineConversionUploadClientSummary#job_summaries).
If instead you let the Google Ads API assign a system-generated value to the `job_id`
of each request, the `job_summaries` contains a separate entry for each request,
which could make analyzing the overall health of your job more challenging.

### Don't use external attribution data

When using enhanced conversions for leads, don't set `external_attribution_data`
on the `ClickConversion` or specify a `conversion_action` that uses an external
attribution model. Google Ads doesn't support externally attributed conversions for
imports using user-provided data.

### Don't include custom variables

When using enhanced conversions for leads, do not include any
[`custom_variables`](https://developers.google.com/google-ads/api/reference/rpc/v26/ClickConversion#custom_variables). Google Ads does not support the use of custom
variables with user-provided data in conversion imports. If you include custom
variables, Google Ads ignores the user-provided data.

## Troubleshooting

[Offline data diagnostics](https://developers.google.com/google-ads/api/docs/conversions/upload-summaries) provide a single resource
for reviewing the overall health of your imports on an ongoing basis. However,
during implementation you can use the information in this section to investigate
any errors reported in the [`partial_failure_error`](https://developers.google.com/google-ads/api/reference/rpc/v26/UploadClickConversionsResponse#partial_failure_error)
field of the response.

Some of the most common errors when importing conversion actions are
authorization errors, such as `USER_PERMISSION_DENIED`. Double check that you
have the customer ID in your request set to the Google Ads conversion customer which
owns the conversion action. Visit our [authorization guide](https://developers.google.com/google-ads/api/docs/oauth/overview)
for more details and see our [common errors guide](https://developers.google.com/google-ads/api/docs/common-errors#authorizationerror) for tips
on how to debug these different errors.

### Debug common errors

The following guidance helps you diagnose and fix common errors with enhanced
conversions for leads.

| Error ||
|---|---|
| [`NO_CONVERSION_ACTION_FOUND`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadErrorEnum.ConversionUploadError#no_conversion_action_found) | The specified conversion action is either not enabled, or cannot be accessed by the client account specified by the `customer_id` field in the request. Make sure the conversion action in your upload is enabled and is owned by the customer sending the upload request. This error may also occur if the GCLID in the request belongs to a client account that does not have access to the conversion action specified in the request. You can verify whether a GCLID belongs to a client account using the [`click_view`](https://developers.google.com/google-ads/api/fields/v26/click_view) resource, by submitting a query that filters by `click_view.gclid` and `segments.date`, where the date is the date the click occurred. |
| [`INVALID_CONVERSION_ACTION_TYPE`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadErrorEnum.ConversionUploadError#invalid_conversion_action_type) | The specified conversion action has a type that is not valid for enhanced conversions for leads. Make sure the `ConversionAction` specified in your upload request has type `UPLOAD_CLICKS`. |
| [`CUSTOMER_NOT_ENABLED_ENHANCED_CONVERSIONS_FOR_LEADS`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadErrorEnum.ConversionUploadError#customer_not_enabled_enhanced_conversions_for_leads) | Make sure you've enabled enhanced conversions for leads in your conversion settings. Find instructions for this in the [prerequisites](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#prerequisites) guide. |
| [`DUPLICATE_ORDER_ID`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadErrorEnum.ConversionUploadError#duplicate_order_id) | Imported events include multiple conversions with the same Order ID and were not processed. Make sure Order IDs are unique and try again. |
| [`CLICK_NOT_FOUND`](https://developers.google.com/google-ads/api/reference/rpc/v26/ConversionUploadErrorEnum.ConversionUploadError#click_not_found) | No click was found that matched the provided user identifiers. > [!NOTE] > **Note:** Treat this error as a warning unless it occurs with the majority of your conversions. If a conversion encounters this warning, the Google Ads API includes it in the `successful_event_count` of your [offline data diagnostics](https://developers.google.com/google-ads/api/docs/conversions/upload-summaries). The Google Ads API includes an entry for `CLICK_NOT_FOUND` in the `https://developers.google.com/google-ads/api/reference/rpc/v26/OfflineConversionUploadClientSummary#alerts[]` collection so you can monitor the frequency of this warning. This error is expected if the click is not from a Google Ads campaign. For example it may come from [SA360](https://marketingplatform.google.com/about/search-ads-360/) or [DV360](https://marketingplatform.google.com/about/display-video-360/). Other possible causes are as follows: - The uploading customer is different from the [Google Ads conversion customer.](https://developers.google.com/google-ads/api/docs/conversions/getting-started#conversion_tracking_setup) - [Tagging](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#tagging) is not correctly configured for your lead form. - The uploaded data has not been correctly [normalized and hashed](https://developers.google.com/google-ads/api/docs/conversions/upload-offline#prepare-data). In rare instances where the uploading customer is different from the Google Ads conversion customer, this error can mean that the uploading customer has accepted the customer data terms, but the serving customer has not. You can determine if an account has accepted the customer data terms by querying the [`customer`](https://developers.google.com/google-ads/api/fields/v26/customer) resource and checking the `customer.conversion_tracking_setting.accepted_customer_data_terms` field. |