Work through this guide to get familiar with the process of sending [Google Ads
multi-source conversions](https://support.google.com/google-ads/answer/16542291) with the Data Manager API.

In this guide, you complete the following steps:

1. Prepare a [`Destination`](https://developers.google.com/data-manager/api/reference/rest/v1/Destination) to receive event data.
2. Prepare event data to send.
3. Build an [`IngestionService`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest) request for events.
4. Send the request with the Google APIs Explorer.
5. Understand success and failure responses.

## Prepare destinations

Before you can send data, you need to prepare at least one
[`Destination`](https://developers.google.com/data-manager/api/reference/rest/v1/Destination) for the data. Here are the fields of a
`Destination`. Check out [Configure destinations](https://developers.google.com/data-manager/api/devguides/concepts/destinations)
for more details and examples of destinations for different scenarios.

Select the tab that corresponds to your use case.

- Select **Advertiser** if you're using credentials for a Google Account that is a user in the advertiser accounts you want to manage.
- Select **Data Partner** if you're using credentials for a Google Account that is a user in a data partner account, and you want to manage advertiser accounts that have a [partner
  link](https://developers.google.com/data-manager/api/devguides/accounts/partner-links) to the data partner account. Data partner accounts are issued only after going through the approval process. To get started, fill out the [interest form](https://docs.google.com/forms/d/e/1FAIpQLScFEkLVqElsjYJ21h7z0mSUk8Tj1M3TEMwouFPQb7qyM-oI5w/viewform).

<br />

### Advertiser

`operatingAccount`

:   The account that receives the events. Must be the Google Ads account that
    owns the conversion action.

    Set `accountType` to `GOOGLE_ADS` and `accountId` to your 10-digit Google
    Ads customer ID (without hyphens).

`loginAccount`

:   The account where the Google Account for the credentials is a user.

`productDestinationId`

:   The ID of the Google Ads conversion action in the `operatingAccount` that
    receives the events.

    The conversion action must have
    [`type`](https://developers.google.com/google-ads/api/reference/rpc/latest/ConversionAction#type)
    set to `WEBPAGE`. In the Google Ads UI, the
    **Conversion source** for a `WEBPAGE`
    conversion action is **Website**.

    {
      "operatingAccount": {
        "accountType": "GOOGLE_ADS",
        "accountId": "OPERATING_ACCOUNT_ID"
      },
      "loginAccount": {
        "accountType": "GOOGLE_ADS",
        "accountId": "LOGIN_ACCOUNT_ID"
      },
      "productDestinationId": "CONVERSION_ACTION_ID"
    }

### Data Partner

`operatingAccount`

:   The account that receives the events. Must be the Google Ads account that
    owns the conversion action.

    Set `accountType` to `GOOGLE_ADS` and `accountId` to your 10-digit Google
    Ads customer ID (without hyphens).

`loginAccount`

:   The account where the credential's user has access.

    Set the `accountId` to your data partner account ID, and set the
    `accountType` to `DATA_PARTNER`. Your data partner account ID is provided
    by Google once your partner request is approved.

`linkedAccount`

:   The account with an established [partner
    link](https://developers.google.com/data-manager/api/devguides/accounts/partner-links) through which the
    credential's user has access to the `operatingAccount`.

    If a *parent* of the `operatingAccount` is linked to your data partner
    account, set the `linkedAccount` to the parent of the `operatingAccount`.
    If the `operatingAccount` is directly linked to your data partner account,
    don't set `linkedAccount`.

`productDestinationId`

:   The ID of the Google Ads conversion action in the `operatingAccount` that
    receives the events.

    The conversion action must have
    [`type`](https://developers.google.com/google-ads/api/reference/rpc/latest/ConversionAction#type)
    set to `WEBPAGE`. In the Google Ads UI, the
    **Conversion source** for a `WEBPAGE`
    conversion action is **Website**.

    {
      "operatingAccount": {
        "accountType": "GOOGLE_ADS",
        "accountId": "OPERATING_ACCOUNT_ID"
      },
      "loginAccount": {
        "accountType": "DATA_PARTNER",
        "accountId": "DATA_PARTNER_ACCOUNT_ID"
      },
      "linkedAccount": {
        "accountType": "GOOGLE_ADS",
        "accountId": "LINKED_ACCOUNT_ID"
      },
      "productDestinationId": "CONVERSION_ACTION_ID"
    }

The example in this guide shows how to construct a request that sends every
event to the same destination. If you want to send events for multiple
destinations in the same request, see [send events for multiple
destinations](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#multiple-destinations).

## Prepare event data

Before sending event data in a request, you must prepare the data:

1. Format raw values as specified in the [formatting
   guide](https://developers.google.com/data-manager/api/devguides/concepts/formatting).
2. User identifiers such as email addresses, given names and family names must be hashed using the [SHA-256 algorithm](https://en.wikipedia.org/wiki/SHA-2) and encoded using hexadecimal (hex) or Base64 encoding.
3. Construct your [`Event`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#event) payload using the formatted and hashed values.

The following table shows example event data progressing from raw input, through
formatting, to the final payload value:


| Event attribute | Raw value | Formatted | Payload value |
|---|---|---|---|
| `event_timestamp` | `2025-06-10 15:07:01-05:00` | `2025-06-10T15:07:01-05:00` *(ISO 8601)* | `2025-06-10T15:07:01-05:00` |
| `product_destination_id` | `123456789` | `123456789` | `123456789` |
| `transaction_id` | `ABC798654321` | `ABC798654321` | `ABC798654321` |
| `conversion_value` | `30.03` | `30.03` | `30.03` |
| `currency` | `USD` | `USD` | `USD` |
| `gclid` | `GCLID_1` | `GCLID_1` | `GCLID_1` |
| `email_address` | `john.smith@EXAMPLE.COM` | `john.smith@example.com` *(trimmed, lowercase)* | `8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0` *(SHA-256 hex)* |
| `phone_number` | `+1 (800) 555-0199` | `+18005550199` *(E.164)* | `78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90` *(SHA-256 hex)* |
| `given_name` | `John` | `john` *(trimmed, lowercase)* | `96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A` *(SHA-256 hex)* |
| `family_name` | `Smith-Jones` | `smith-jones` *(trimmed, lowercase)* | `DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081` *(SHA-256 hex)* |
| `region_code` | `us` | `US` *(2-letter ISO code)* | `US` |
| `postal_code` | `94045` | `94045` | `94045` |
| `customer_type` | `NEW` | `NEW` | `NEW` |
| `customer_value_bucket` | `HIGH` | `HIGH` | `HIGH` |
| `item_id` | `SKU_12345` | `SKU_12345` | `SKU_12345` |
| `merchant_product_id` | `12345` | `12345` | `12345` |
| `unit_price` | `10.01` | `10.01` | `10.01` |
| `quantity` | `3` | `3` | `3` |

<br />

> [!NOTE]
> **Note:** The examples in this guide don't use encryption, but you can follow the instructions in [Encrypt user data](https://developers.google.com/data-manager/api/devguides/concepts/encryption) to add encryption to your process.

## Convert the data to `Event` objects

Convert each event's formatted and hashed data to an [`Event`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#event).

### Event field requirements

Construct your events according to the requirements in the following table.
Refer to the [`Event`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#event) reference documentation for the complete list of
available fields.

| Field | Status | Description |
|---|---|---|
| [`eventTimestamp`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.event_timestamp) | **Required** | The time the event occurred. See [Timestamp format](https://developers.google.com/data-manager/api/devguides/concepts/formatting#timestamp_format). |
| [`transactionId`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.transaction_id) | **Required** | A unique identifier for the conversion event, used for [deduplication](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#update-semantics) against Google tag conversions. |
| [`eventSource`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.event_source) | Optional | The origin of the event. If set, must be `WEB`. |
| [`conversionValue`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.conversion_value) | Optional | The monetary value associated with the conversion. |
| [`currency`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.currency) | Optional | The 3-letter [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code (such as `USD` or `EUR`) associated with monetary values in this event. |
| [`cartData`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#cartdata) | Optional | Associated shopping cart and item-level purchase data. If provided, at least one [item](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#add_item) in `items` is **Required** . See [Add cart data](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#add_cart_data). |
| [`destinationReferences`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.destination_references) | **Conditionally Required** | Required only when [sending events to multiple destinations](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#multiple-destinations) in a single request. |
| [`userProperties`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.user_properties) | Optional | [Information about the user assessed at the time of the event](https://support.google.com/google-ads/answer/14007601), like `customerType` (`NEW` or `RETURNING`) or `customerValueBucket`. |
| [`userId`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.user_id) | Optional | A unique identifier for a user. |
| [`consent`](https://developers.google.com/data-manager/api/reference/rest/v1/Consent) | Optional | [Digital Markets Act (DMA)](https://digital-markets-act.ec.europa.eu/index_en) consent settings for the user, specifying whether consent is granted for `adUserData` and `adPersonalization`. |

### Identifier requirements

**Required.** You must set at least one of the following:

- [`adIdentifiers`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#adidentifiers) with at least one of the following provided:
  - `gclid`
  - `gbraid` or `wbraid`
  - `landingPageDeviceInfo.ipAddress`
- [Session attributes](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#session_attributes)
- [`userData`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.user_data)
- [`eventDeviceInfo.ipAddress`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.event_device_info)


> [!NOTE]
> **Note:** If you provide an address in `userIdentifiers.address`, you must include all four of `givenName`, `familyName`, `regionCode`, and `postalCode`. The `addressLine`, `city`, and `administrativeArea` subfields are used only for Google Analytics.

<br />

### How Google handles multi-source data

Within the same conversion action, Google Ads uses the
[`transactionId`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.transaction_id)
to deduplicate conversion events sent from different
sources (like your website tag and Data Manager API ingestion requests). The
following table explains how the data from your ingestion requests is
processed.

| Scenario | Data field | How it's handled |
|---|---|---|
| `transactionId` MATCHES an existing tag event | `conversionValue` (with `currency`) | **Updated.** The `conversionValue` (with `currency`) from the `Event` overrides the original value recorded by the tag. **Note:** During the initial 14-day trial period for a conversion action, value updates are disabled. The tag's value won't be overridden in Google Ads reporting until the trial period ends. |
| `transactionId` MATCHES an existing tag event | User-provided data in `userData`, such as email address, phone number, or street address. | - **Supplemented if missing:** If the Google tag captured the conversion but failed to capture any user-provided data, the system will successfully insert the data provided by your offline upload. > [!NOTE] > **Note:** We highly recommend uploading this offline data within 24 hours of the initial tag event for optimal performance. - **Ignored if tag data exists:** If the Google tag successfully captured user-provided data on-site, any subsequent or different data uploaded offline for that same transaction is ignored (it won't overwrite or append to the existing tag data). |
| `transactionId` MATCHES an existing tag event | Other fields except `conversionValue`, `currency`, or `userData` (for example, `adIdentifiers.gclid`) | **Ignored.** Other field values from your additional data source won't overwrite the field values originally recorded by your Google tag for matched transactions. |
| `transactionId` does NOT match any existing event | All provided data (for example, `userData`, `conversionValue`, `currency`) | **Used to create a new conversion event.** Google will then try to attribute this new conversion to an ad click using the identifiers you provided (like `adIdentifiers.gclid` or `userData`). **Note:** During the initial 14-day trial period, these newly created conversions will appear in your reporting but won't be used for bidding. After the trial ends, they will automatically become biddable. |

### Add session attributes

Add [session attributes](https://support.google.com/google-ads/answer/16194756) when
other ad identifiers such as a GCLID or WBRAID aren't available. You can also
include session attributes in addition to other ad identifiers.

Session attributes provide further context and signals about the user's
interaction with your website, which can enhance conversion measurement,
reporting and bidding accuracy.

Choose an approach for sending session attributes:

- **Recommended:** Set the
  [`sessionAttributes`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#AdIdentifiers.FIELDS.session_attributes)
  field of `adIdentifiers` to the base64 encoded session attributes string
  [captured in a form submission](https://support.google.com/google-ads/answer/16194756#capture).

- **Alternative:** Capture the individual session attribute fields and add
  each attribute to the event in the
  [`experimentalFields`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.experimental_fields)
  and [`adIdentifiers`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#adidentifiers) fields.
  Only use this option if you can't capture the encoded session attributes
  string.

### Recommended

Set the
[`sessionAttributes`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#AdIdentifiers.FIELDS.session_attributes)
field of `adIdentifiers` to the base64 encoded session attributes string.
Follow the instructions in [How to capture
session_attributes](https://support.google.com/google-ads/answer/16194756#capture)
to modify your form submission pages to capture the encoded string.

Here's a portion of a sample event with encoded session attributes in the
[`sessionAttributes`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#AdIdentifiers.FIELDS.session_attributes)
field:

    {
      ...,
      "adIdentifiers": {
        "sessionAttributes": "INSERT_BASE64_ENCODED_SESSION_ATTRIBUTES_STRING_HERE"
      }
    }

### Alternative

If you can't capture the encoded session attributes string, then add an
[`ExperimentalField`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#experimentalfield) for
each session attribute to the
[`experimentalFields`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#Event.FIELDS.experimental_fields)
list. In addition, add the user agent to the
[`landingPageDeviceInfo`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#AdIdentifiers.FIELDS.landing_page_device_info)
field of the [`adIdentifiers`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#adidentifiers)
field.

`experimentalFields`:

:   **Required:** Add a key-value pair entry to `experimentalFields` for each
    required key:

    - `gad_source`:
    - `gad_campaignid`
    - `session_start_time_usec`

    In addition, you can add a key-value pair for each optional key:

    - `landing_page_url`

      > [!TIP]
      > **Tip:** Don't provide an inaccurate or partial `landing_page_url` value such as a placeholder string, an internal application path, or an incomplete URL. Omit the `landing_page_url` if you don't have the accurate, full URL.

    - `landing_page_referrer`

`adIdentifiers`:

:   **Required:** Include the landing page user agent in the `userAgent`
    field of
    [`adIdentifiers.landingPageDeviceInfo`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#AdIdentifiers.FIELDS.landing_page_device_info).

Here's a portion of a sample event with entries in `experimentalFields` for
`gad_source`, `gad_campaignid` and `session_start_time_usec`, and the user
agent in the `landingPageDeviceInfo` field:

    {
      ...,
      "experimentalFields": [
        {
          "field": "gad_source",
          "value": "1"
        },
        {
          "field": "gad_campaignid",
          "value": "21288051566"
        },
        {
          "field": "session_start_time_usec",
          "value": "1767711548052000"
        }
      ],
      "adIdentifiers": {
        "landingPageDeviceInfo": {
          "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
        }
      }
    }

### Add cart data

Populate the `cartData` field of the [`Event`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#event) with information about the items
associated with the event. Use this field when sending Google Ads
[conversions with cart data parameters](https://support.google.com/google-ads/answer/14943675).

Here are the fields of the [`CartData`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#cartdata) object:

`items`
:   **Required** . Include at least one [item](https://developers.google.com/data-manager/api/devguides/events/google-ads/online/send-events#add_item) in this list.

#### Merchant Center fields

The `merchantId`, `merchantFeedLabel`, and `merchantFeedLanguageCode` are all
optional. Set these fields if the items in the event include products that
exist in several Merchant Center accounts.

`merchantId`
:   Optional. The Merchant Center account ID.

`merchantFeedLabel`
:   Optional. The [feed label](https://support.google.com/merchants/answer/14994087) of
    the Merchant Center feed. Feed labels allow you to categorize products for
    campaign targeting. For example, you can use feed labels to organize products
    by language. If your campaign targets products based on country, use the
    2-letter country code in ISO-3166-1 alpha-2 format. Example: `US`.

`merchantFeedLanguageCode`
:   Optional. The ISO 639-1 language code associated with the Merchant Center feed
    where your items are uploaded. Example: `en`.

#### Item fields

Add one or more [`Item`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#item) objects to the `items` list of [`CartData`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#cartdata). The
`items` list must not be empty when `cartData` is provided.

Populate the following fields for each `Item`:

`items.itemId`
:   **Required**. A unique identifier for the item.

`items.merchantProductId`
:   **Required** . The [product ID](https://support.google.com/merchants/answer/6324405)
    within the Merchant Center account.

`items.unitPrice`

:   **Required** . The unit price excluding tax, shipping, and *event-scoped*
    (transaction-level) discounts for this item.

    If the item has an *item-scoped* discount, use the *discounted* unit
    price. For example, if an item has a unit price of `27.67` and a unit
    discount of `6.66`, set `unitPrice` to `21.01`.

`items.quantity`

:   **Required**. The quantity of units purchased for this particular item.

### Sample requests

Here's a sample `Event` for the formatted, hashed, and encoded data from the
event, as an example of a Google Ads multi-source conversion with user data:

      {
      "adIdentifiers": {
        "gclid": "GCLID_1"
      },
      "conversionValue": 30.03,
      "currency": "USD",
      "eventTimestamp": "2025-06-10T15:07:01-05:00",
      "transactionId": "ABC798654321",
      "eventSource": "WEB",
      "userData": {
        "userIdentifiers": [
          {
            "emailAddress": "8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0"
          },
          {
            "phoneNumber": "78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90"
          },
          {
            "address": {
              "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A",
              "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
              "regionCode": "US",
              "postalCode": "94045"
            }
          }
        ]
      },
      "userProperties": {
        "customerType": "NEW",
        "customerValueBucket": "HIGH"
      }
    }

## Build the request body

To build the request body, combine the `destinations` and `events`, set the
`encoding` field, and add any other request fields you want to include such as
`validateOnly` and `consent`.

## Send the request

Here are the steps to try a request from your browser:

1. Select the **REST** tab and click **Open in API Explorer** to open the API Explorer in a new tab or window.
2. In the request body in the API Explorer, replace each string beginning with `REPLACE_WITH`, such as `REPLACE_WITH_OPERATING_ACCOUNT_TYPE`, with the relevant value.
3. Click **Execute** at the bottom of the API Explorer page and complete the authorization prompts to send the request.
4. Set `validateOnly` to `true` to validate the request without applying the changes. When you're ready to apply the changes, set `validateOnly` to `false`.

If you [installed a client library](https://developers.google.com/data-manager/api/devguides/quickstart/install-library),
select the tab for your chosen programming language to see a complete code
sample of how to construct and send a request.

### REST

> [!NOTE]
> **Note:** Due to URL length limitations, the **Open in API Explorer** functionality will only pre-populate the request body with the *first* event from the following example.

### Advertiser


[Open in API Explorer](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest?apix=true&apix_params=%0A%7B%22resource%22:%7B%22destinations%22:%5B%7B%22operatingAccount%22:%7B%22accountType%22:%22REPLACE_WITH_OPERATING_ACCOUNT_TYPE%22,%22accountId%22:%22REPLACE_WITH_OPERATING_ACCOUNT_ID%22%7D,%22loginAccount%22:%7B%22accountType%22:%22REPLACE_WITH_LOGIN_ACCOUNT_TYPE%22,%22accountId%22:%22REPLACE_WITH_LOGIN_ACCOUNT_ID%22%7D,%22productDestinationId%22:%22REPLACE_WITH_CONVERSION_ACTION_ID%22%7D%5D,%22encoding%22:%22HEX%22,%22consent%22:%7B%22adUserData%22:%22CONSENT_GRANTED%22,%22adPersonalization%22:%22CONSENT_GRANTED%22%7D,%22events%22:%5B%7B%22adIdentifiers%22:%7B%22gclid%22:%22GCLID_1%22%7D,%22conversionValue%22:30.03,%22currency%22:%22USD%22,%22eventTimestamp%22:%222025-06-10T15:07:01-05:00%22,%22transactionId%22:%22ABC798654321%22,%22eventSource%22:%22WEB%22,%22userData%22:%7B%22userIdentifiers%22:%5B%7B%22emailAddress%22:%228E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0%22%7D,%7B%22phoneNumber%22:%2278724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90%22%7D,%7B%22address%22:%7B%22givenName%22:%2296D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A%22,%22familyName%22:%22DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081%22,%22regionCode%22:%22US%22,%22postalCode%22:%2294045%22%7D%7D%5D%7D,%22userProperties%22:%7B%22customerType%22:%22NEW%22,%22customerValueBucket%22:%22HIGH%22%7D%7D%5D,%22validateOnly%22:true%7D%7D "Open this sample in API Explorer")

```json
{
    "destinations": [
        {
            "operatingAccount": {
                "accountType": "OPERATING_ACCOUNT_TYPE",
                "accountId": "OPERATING_ACCOUNT_ID"
            },
            "loginAccount": {
                "accountType": "LOGIN_ACCOUNT_TYPE",
                "accountId": "LOGIN_ACCOUNT_ID"
            },
            "productDestinationId": "CONVERSION_ACTION_ID"
        }
    ],
    "encoding": "HEX",
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "events": [
        {
            "adIdentifiers": {
                "gclid": "GCLID_1"
            },
            "conversionValue": 30.03,
            "currency": "USD",
            "eventTimestamp": "2025-06-10T15:07:01-05:00",
            "transactionId": "ABC798654321",
            "eventSource": "WEB",
            "userData": {
                "userIdentifiers": [
                    {
                        "emailAddress": "8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0"
                    },
                    {
                        "phoneNumber": "78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90"
                    },
                    {
                        "address": {
                            "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A",
                            "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
                            "regionCode": "US",
                            "postalCode": "94045"
                        }
                    }
                ]
            },
            "userProperties": {
                "customerType": "NEW",
                "customerValueBucket": "HIGH"
            }
        }
    ],
    "validateOnly": true
}
```

### Data Partner


[Open in API Explorer](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest?apix=true&apix_params=%0A%7B%22resource%22:%7B%22destinations%22:%5B%7B%22operatingAccount%22:%7B%22accountType%22:%22REPLACE_WITH_OPERATING_ACCOUNT_TYPE%22,%22accountId%22:%22REPLACE_WITH_OPERATING_ACCOUNT_ID%22%7D,%22loginAccount%22:%7B%22accountType%22:%22DATA_PARTNER%22,%22accountId%22:%22REPLACE_WITH_DATA_PARTNER_ACCOUNT_ID%22%7D,%22linkedAccount%22:%7B%22accountType%22:%22REPLACE_WITH_LINKED_ACCOUNT_TYPE%22,%22accountId%22:%22REPLACE_WITH_LINKED_ACCOUNT_ID%22%7D,%22productDestinationId%22:%22REPLACE_WITH_CONVERSION_ACTION_ID%22%7D%5D,%22encoding%22:%22HEX%22,%22consent%22:%7B%22adUserData%22:%22CONSENT_GRANTED%22,%22adPersonalization%22:%22CONSENT_GRANTED%22%7D,%22events%22:%5B%7B%22adIdentifiers%22:%7B%22gclid%22:%22GCLID_1%22%7D,%22conversionValue%22:30.03,%22currency%22:%22USD%22,%22eventTimestamp%22:%222025-06-10T15:07:01-05:00%22,%22transactionId%22:%22ABC798654321%22,%22eventSource%22:%22WEB%22,%22userData%22:%7B%22userIdentifiers%22:%5B%7B%22emailAddress%22:%228E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0%22%7D,%7B%22phoneNumber%22:%2278724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90%22%7D,%7B%22address%22:%7B%22givenName%22:%2296D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A%22,%22familyName%22:%22DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081%22,%22regionCode%22:%22US%22,%22postalCode%22:%2294045%22%7D%7D%5D%7D,%22userProperties%22:%7B%22customerType%22:%22NEW%22,%22customerValueBucket%22:%22HIGH%22%7D%7D%5D,%22validateOnly%22:true%7D%7D "Open this sample in API Explorer")

```json
{
    "destinations": [
        {
            "operatingAccount": {
                "accountType": "OPERATING_ACCOUNT_TYPE",
                "accountId": "OPERATING_ACCOUNT_ID"
            },
            "loginAccount": {
                "accountType": "DATA_PARTNER",
                "accountId": "DATA_PARTNER_ACCOUNT_ID"
            },
            "linkedAccount": {
                "accountType": "LINKED_ACCOUNT_TYPE",
                "accountId": "LINKED_ACCOUNT_ID"
            },
            "productDestinationId": "CONVERSION_ACTION_ID"
        }
    ],
    "encoding": "HEX",
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "events": [
        {
            "adIdentifiers": {
                "gclid": "GCLID_1"
            },
            "conversionValue": 30.03,
            "currency": "USD",
            "eventTimestamp": "2025-06-10T15:07:01-05:00",
            "transactionId": "ABC798654321",
            "eventSource": "WEB",
            "userData": {
                "userIdentifiers": [
                    {
                        "emailAddress": "8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0"
                    },
                    {
                        "phoneNumber": "78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90"
                    },
                    {
                        "address": {
                            "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A",
                            "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
                            "regionCode": "US",
                            "postalCode": "94045"
                        }
                    }
                ]
            },
            "userProperties": {
                "customerType": "NEW",
                "customerValueBucket": "HIGH"
            }
        }
    ],
    "validateOnly": true
}
```

### .NET

```
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Text.Json;
using CommandLine;
using Google.Ads.DataManager.Util;
using Google.Ads.DataManager.V1;
using Google.Protobuf.WellKnownTypes;
using static Google.Ads.DataManager.V1.ProductAccount.Types;

namespace Google.Ads.DataManager.Samples
{
    // <summary>
    // Sends an <see cref="IngestEventsRequest" /> without using encryption.
    //
    // Event data is read from a data file. See the <c>events_1.json</c> file in the
    // <c>sampledata</c> directory for an example.
    // </summary>
    public class IngestEvents
    {
        private static readonly int MaxEventsPerRequest = 2_000;

        [Verb("ingest-events", HelpText = "Sends an IngestEventsRequest without using encryption.")]
        public class Options
        {
            [Option(
                "operatingAccountType",
                Required = true,
                HelpText = "Account type of the operating account"
            )]
            public AccountType OperatingAccountType { get; set; }

            [Option(
                "operatingAccountId",
                Required = true,
                HelpText = "ID of the operating account"
            )]
            public string OperatingAccountId { get; set; } = null!;

            [Option(
                "loginAccountType",
                Required = false,
                HelpText = "Account type of the login account"
            )]
            public AccountType? LoginAccountType { get; set; }

            [Option("loginAccountId", Required = false, HelpText = "ID of the login account")]
            public string? LoginAccountId { get; set; }

            [Option(
                "linkedAccountProduct",
                Required = false,
                HelpText = "Account type of the linked account"
            )]
            public AccountType? LinkedAccountType { get; set; }

            [Option("linkedAccountId", Required = false, HelpText = "ID of the linked account")]
            public string? LinkedAccountId { get; set; }

            [Option(
                "conversionActionId",
                Required = true,
                HelpText = "ID of the conversion action"
            )]
            public string ConversionActionId { get; set; } = null!;

            [Option(
                "jsonFile",
                Required = true,
                HelpText = "JSON file containing user data to ingest"
            )]
            public string JsonFile { get; set; } = null!;

            [Option(
                "validateOnly",
                Default = true,
                HelpText = "Whether to enable validateOnly on the request"
            )]
            public bool ValidateOnly { get; set; }
        }

        public void Run(Options options)
        {
            RunExample(
                options.OperatingAccountType,
                options.OperatingAccountId,
                options.LoginAccountType,
                options.LoginAccountId,
                options.LinkedAccountType,
                options.LinkedAccountId,
                options.ConversionActionId,
                options.JsonFile,
                options.ValidateOnly
            );
        }

        private void RunExample(
            AccountType operatingAccountType,
            string operatingAccountId,
            AccountType? loginAccountType,
            string? loginAccountId,
            AccountType? linkedAccountType,
            string? linkedAccountId,
            string conversionActionId,
            string jsonFile,
            bool validateOnly
        )
        {
            if (loginAccountId == null ^ loginAccountType == null)
            {
                throw new ArgumentException(
                    "Must specify either both or neither of login account ID and login account "
                        + "type"
                );
            }
            if (linkedAccountId == null ^ linkedAccountType == null)
            {
                throw new ArgumentException(
                    "Must specify either both or neither of linked account ID and linked account "
                        + "type"
                );
            }

            // Reads member data from the data file.
            List<EventRecord> eventRecords = ReadEventData(jsonFile);
            // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
            UserDataFormatter userDataFormatter = new UserDataFormatter();

            // Builds the events collection for the request.
            var events = new List<Event>();
            foreach (var eventRecord in eventRecords)
            {
                var eventBuilder = new Event();

                try
                {
                    eventBuilder.EventTimestamp = Timestamp.FromDateTime(
                        DateTime.Parse(eventRecord.Timestamp ?? "").ToUniversalTime()
                    );
                }
                catch (FormatException)
                {
                    Console.WriteLine(
                        $"Skipping event with invalid timestamp: {eventRecord.Timestamp}"
                    );
                    continue;
                }

                if (string.IsNullOrEmpty(eventRecord.TransactionId))
                {
                    Console.WriteLine("Skipping event with no transaction ID");
                    continue;
                }
                eventBuilder.TransactionId = eventRecord.TransactionId;

                if (!string.IsNullOrEmpty(eventRecord.EventSource))
                {
                    if (
                        System.Enum.TryParse(
                            eventRecord.EventSource,
                            true,
                            out EventSource eventSource
                        )
                    )
                    {
                        eventBuilder.EventSource = eventSource;
                    }
                    else
                    {
                        Console.WriteLine(
                            $"Skipping event with invalid event source: {eventRecord.EventSource}"
                        );
                        continue;
                    }
                }

                if (!string.IsNullOrEmpty(eventRecord.Gclid))
                {
                    eventBuilder.AdIdentifiers = new AdIdentifiers { Gclid = eventRecord.Gclid };
                }

                if (!string.IsNullOrEmpty(eventRecord.Currency))
                {
                    eventBuilder.Currency = eventRecord.Currency;
                }

                if (eventRecord.Value.HasValue)
                {
                    eventBuilder.ConversionValue = eventRecord.Value.Value;
                }

                var userDataBuilder = new UserData();

                // Adds a UserIdentifier for each valid email address for the eventRecord.
                if (eventRecord.Emails != null)
                {
                    foreach (var email in eventRecord.Emails)
                    {
                        try
                        {
                            string preparedEmail = userDataFormatter.ProcessEmailAddress(
                                email,
                                UserDataFormatter.Encoding.Hex
                            );
                            // Adds an email address identifier with the encoded email hash.
                            userDataBuilder.UserIdentifiers.Add(
                                new UserIdentifier { EmailAddress = preparedEmail }
                            );
                        }
                        catch (ArgumentException)
                        {
                            // Skips invalid input.
                            continue;
                        }
                    }
                }

                // Adds a UserIdentifier for each valid phone number for the eventRecord.
                if (eventRecord.PhoneNumbers != null)
                {
                    foreach (var phoneNumber in eventRecord.PhoneNumbers)
                    {
                        try
                        {
                            string preparedPhoneNumber = userDataFormatter.ProcessPhoneNumber(
                                phoneNumber,
                                UserDataFormatter.Encoding.Hex
                            );
                            // Adds a phone number identifier with the encoded phone hash.
                            userDataBuilder.UserIdentifiers.Add(
                                new UserIdentifier { PhoneNumber = preparedPhoneNumber }
                            );
                        }
                        catch (ArgumentException)
                        {
                            // Skips invalid input.
                            continue;
                        }
                    }
                }

                if (userDataBuilder.UserIdentifiers.Any())
                {
                    eventBuilder.UserData = userDataBuilder;
                }
                events.Add(eventBuilder);
            }

            // Builds the Destination for the request.
            var destinationBuilder = new Destination
            {
                OperatingAccount = new ProductAccount
                {
                    AccountType = operatingAccountType,
                    AccountId = operatingAccountId,
                },
                ProductDestinationId = conversionActionId,
            };

            if (loginAccountType.HasValue && loginAccountId != null)
            {
                destinationBuilder.LoginAccount = new ProductAccount
                {
                    AccountType = loginAccountType.Value,
                    AccountId = loginAccountId,
                };
            }

            if (linkedAccountType.HasValue && linkedAccountId != null)
            {
                destinationBuilder.LinkedAccount = new ProductAccount
                {
                    AccountType = linkedAccountType.Value,
                    AccountId = linkedAccountId,
                };
            }

            IngestionServiceClient ingestionServiceClient = IngestionServiceClient.Create();

            int requestCount = 0;

            // Batches requests to send up to the maximum number of events per request.
            for (var i = 0; i < events.Count; i += MaxEventsPerRequest)
            {
                IEnumerable<Event> batch = events.Skip(i).Take(MaxEventsPerRequest);
                requestCount++;
                var request = new IngestEventsRequest
                {
                    Destinations = { destinationBuilder },
                    // Adds events from the current batch.
                    Events = { batch },
                    Consent = new Consent
                    {
                        AdPersonalization = ConsentStatus.ConsentGranted,
                        AdUserData = ConsentStatus.ConsentGranted,
                    },
                    // Sets validate_only. If true, then the Data Manager API only validates the
                    // request but doesn't apply changes.
                    ValidateOnly = validateOnly,
                    Encoding = V1.Encoding.Hex,
                };

                // Sends the data to the Data Manager API.
                IngestEventsResponse response = ingestionServiceClient.IngestEvents(request);
                Console.WriteLine($"Response for request #{requestCount}:\n{response}");

                if (response.FieldWarnings.Any())
                {
                    Console.WriteLine(
                        "Request ingested successfully, but field warnings were returned. "
                            + "Review warning details and update your implementation as needed."
                    );
                }
            }
            Console.WriteLine($"# of requests sent: {requestCount}");
        }

        private class EventRecord
        {
            public List<string>? Emails { get; set; }
            public List<string>? PhoneNumbers { get; set; }
            public string? Timestamp { get; set; }
            public string? TransactionId { get; set; }
            public string? EventSource { get; set; }
            public double? Value { get; set; }
            public string? Currency { get; set; }
            public string? Gclid { get; set; }
        }

        private List<EventRecord> ReadEventData(string jsonFile)
        {
            string jsonString = File.ReadAllText(jsonFile);
            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
            return JsonSerializer.Deserialize<List<EventRecord>>(jsonString, options)
                ?? new List<EventRecord>();
        }
    }
}
```

### Java

```java
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ads.datamanager.samples;

import com.beust.jcommander.Parameter;
import com.google.ads.datamanager.samples.common.BaseParamsConfig;
import com.google.ads.datamanager.util.UserDataFormatter;
import com.google.ads.datamanager.util.UserDataFormatter.Encoding;
import com.google.ads.datamanager.v1.AdIdentifiers;
import com.google.ads.datamanager.v1.Consent;
import com.google.ads.datamanager.v1.ConsentStatus;
import com.google.ads.datamanager.v1.Destination;
import com.google.ads.datamanager.v1.Event;
import com.google.ads.datamanager.v1.EventSource;
import com.google.ads.datamanager.v1.IngestEventsRequest;
import com.google.ads.datamanager.v1.IngestEventsResponse;
import com.google.ads.datamanager.v1.IngestionServiceClient;
import com.google.ads.datamanager.v1.ProductAccount;
import com.google.ads.datamanager.v1.ProductAccount.AccountType;
import com.google.ads.datamanager.v1.UserData;
import com.google.ads.datamanager.v1.UserIdentifier;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import com.google.gson.GsonBuilder;
import com.google.protobuf.util.Timestamps;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Sends an {@link IngestEventsRequest} without using encryption.
 *
 * <p>Event data is read from a data file. See the {@code events_1.json} file in the {@code
 * resources/sampledata} directory for a sample file.
 */
public class IngestEvents {
  private static final Logger LOGGER = Logger.getLogger(IngestEvents.class.getName());

  /** The maximum number of events allowed per request. */
  private static final int MAX_EVENTS_PER_REQUEST = 2_000;

  private static final class ParamsConfig extends BaseParamsConfig<ParamsConfig> {

    @Parameter(
        names = "--operatingAccountType",
        required = true,
        description = "Account type of the operating account")
    AccountType operatingAccountType;

    @Parameter(
        names = "--operatingAccountId",
        required = true,
        description = "ID of the operating account")
    String operatingAccountId;

    @Parameter(
        names = "--loginAccountType",
        required = false,
        description = "Account type of the login account")
    AccountType loginAccountType;

    @Parameter(
        names = "--loginAccountId",
        required = false,
        description = "ID of the login account")
    String loginAccountId;

    @Parameter(
        names = "--linkedAccountType",
        required = false,
        description = "Account type of the linked account")
    AccountType linkedAccountType;

    @Parameter(
        names = "--linkedAccountId",
        required = false,
        description = "ID of the linked account")
    String linkedAccountId;

    @Parameter(
        names = "--conversionActionId",
        required = true,
        description = "ID of the conversion action")
    String conversionActionId;

    @Parameter(
        names = "--jsonFile",
        required = true,
        description = "JSON file containing user data to ingest")
    String jsonFile;

    @Parameter(
        names = "--validateOnly",
        required = false,
        arity = 1,
        description = "Whether to enable validateOnly on the request")
    boolean validateOnly = true;
  }

  public static void main(String[] args) throws IOException {
    ParamsConfig paramsConfig = new ParamsConfig().parseOrExit(args);
    if ((paramsConfig.loginAccountId == null) != (paramsConfig.loginAccountType == null)) {
      throw new IllegalArgumentException(
          "Must specify either both or neither of login account ID and login account type");
    }
    if ((paramsConfig.linkedAccountId == null) != (paramsConfig.linkedAccountType == null)) {
      throw new IllegalArgumentException(
          "Must specify either both or neither of linked account ID and linked account type");
    }
    new IngestEvents().runExample(paramsConfig);
  }

  /**
   * Runs the example. This sample assumes that the login and operating account are the same.
   *
   * @param params the parameters for the example
   */
  private void runExample(ParamsConfig params) throws IOException {
    // Reads event data from the JSON file.
    List<EventRecord> eventRecords = readEventData(params.jsonFile);

    // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
    UserDataFormatter userDataFormatter = UserDataFormatter.create();

    // Builds the events collection for the request.
    List<Event> events = new ArrayList<>();
    for (EventRecord eventRecord : eventRecords) {
      Event.Builder eventBuilder = Event.newBuilder();
      try {
        eventBuilder.setEventTimestamp(Timestamps.parse(eventRecord.timestamp));
      } catch (ParseException pe) {
        LOGGER.warning(
            () ->
                String.format("Skipping event with invalid timestamp: %s", eventRecord.timestamp));
        continue;
      }

      if (Strings.isNullOrEmpty(eventRecord.transactionId)) {
        LOGGER.warning("Skipping event with no transaction ID");
        continue;
      }
      eventBuilder.setTransactionId(eventRecord.transactionId);
      if (!Strings.isNullOrEmpty(eventRecord.eventSource)) {
        try {
          eventBuilder.setEventSource(EventSource.valueOf(eventRecord.eventSource));
        } catch (IllegalArgumentException iae) {
          LOGGER.warning("Skipping event with invalid event source: " + eventRecord.eventSource);
          continue;
        }
      }

      if (!Strings.isNullOrEmpty(eventRecord.gclid)) {
        eventBuilder.setAdIdentifiers(AdIdentifiers.newBuilder().setGclid(eventRecord.gclid));
      }

      if (!Strings.isNullOrEmpty(eventRecord.currency)) {
        eventBuilder.setCurrency(eventRecord.currency);
      }

      if (eventRecord.value != null) {
        eventBuilder.setConversionValue(eventRecord.value);
      }

      UserData.Builder userDataBuilder = UserData.newBuilder();

      // Adds a UserIdentifier for each valid email address for the eventRecord.
      if (eventRecord.emails != null) {
        for (String email : eventRecord.emails) {
          String preparedEmail;
          try {
            preparedEmail = userDataFormatter.processEmailAddress(email, Encoding.HEX);
          } catch (IllegalArgumentException iae) {
            // Skips invalid input.
            continue;
          }
          // Sets the email address identifier to the encoded email hash.
          userDataBuilder.addUserIdentifiers(
              UserIdentifier.newBuilder().setEmailAddress(preparedEmail));
        }
      }

      // Adds a UserIdentifier for each valid phone number for the eventRecord.
      if (eventRecord.phoneNumbers != null) {
        for (String phoneNumber : eventRecord.phoneNumbers) {
          String preparedPhoneNumber;
          try {
            preparedPhoneNumber = userDataFormatter.processPhoneNumber(phoneNumber, Encoding.HEX);
          } catch (IllegalArgumentException iae) {
            // Skips invalid input.
            continue;
          }
          // Sets the phone number identifier to the encoded phone number hash.
          userDataBuilder.addUserIdentifiers(
              UserIdentifier.newBuilder().setPhoneNumber(preparedPhoneNumber));
        }
      }

      if (userDataBuilder.getUserIdentifiersCount() > 0) {
        eventBuilder.setUserData(userDataBuilder);
      }
      events.add(eventBuilder.build());
    }

    // Builds the Destination for the request.
    Destination.Builder destinationBuilder =
        Destination.newBuilder()
            .setOperatingAccount(
                ProductAccount.newBuilder()
                    .setAccountType(params.operatingAccountType)
                    .setAccountId(params.operatingAccountId))
            .setProductDestinationId(params.conversionActionId);
    if (params.loginAccountType != null && params.loginAccountId != null) {
      destinationBuilder.setLoginAccount(
          ProductAccount.newBuilder()
              .setAccountType(params.loginAccountType)
              .setAccountId(params.loginAccountId));
    }
    if (params.linkedAccountType != null && params.linkedAccountId != null) {
      destinationBuilder.setLinkedAccount(
          ProductAccount.newBuilder()
              .setAccountType(params.linkedAccountType)
              .setAccountId(params.linkedAccountId));
    }

    try (IngestionServiceClient ingestionServiceClient = IngestionServiceClient.create()) {
      int requestCount = 0;
      // Batches requests to send up to the maximum number of events per request.
      for (List<Event> eventsBatch : Lists.partition(events, MAX_EVENTS_PER_REQUEST)) {
        requestCount++;
        // Builds the request.
        IngestEventsRequest request =
            IngestEventsRequest.newBuilder()
                .addDestinations(destinationBuilder)
                // Adds events from the current batch.
                .addAllEvents(eventsBatch)
                .setConsent(
                    Consent.newBuilder()
                        .setAdPersonalization(ConsentStatus.CONSENT_GRANTED)
                        .setAdUserData(ConsentStatus.CONSENT_GRANTED))
                // Sets validate_only. If true, then the Data Manager API only validates the request
                // but doesn't apply changes.
                .setValidateOnly(params.validateOnly)
                // Sets encoding to match the encoding used.
                .setEncoding(com.google.ads.datamanager.v1.Encoding.HEX)
                .build();

        LOGGER.info(() -> String.format("Request:%n%s", request));
        IngestEventsResponse response = ingestionServiceClient.ingestEvents(request);
        LOGGER.info(String.format("Response for request #:%n%s", requestCount, response));
      }

      LOGGER.info("# of requests sent: " + requestCount);
    }
  }

  /** Data object for a single row of input data. */
  @SuppressWarnings("unused")
  private static class EventRecord {
    private List<String> emails;
    private List<String> phoneNumbers;
    private String timestamp;
    private String transactionId;
    private String eventSource;
    private Double value;
    private String currency;
    private String gclid;
  }

  /** Reads the data file and parses each line into a {@link EventRecord} object. */
  private List<EventRecord> readEventData(String jsonFile) throws IOException {
    try (BufferedReader jsonReader =
        Files.newBufferedReader(Paths.get(jsonFile), StandardCharsets.UTF_8)) {
      // Define the type for Gson to deserialize into (List of EventRecord objects)
      Type recordListType = new TypeToken<ArrayList<EventRecord>>() {}.getType();

      // Parse the JSON string from the file into a List of EventRecord objects
      return new GsonBuilder().create().fromJson(jsonReader, recordListType);
    }
  }
}
```

### Node

```javascript
#!/usr/bin/env node
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

import {IngestionServiceClient} from '@google-ads/datamanager';
import {protos} from '@google-ads/datamanager';
const {
  Event: DataManagerEvent,
  Destination,
  Encoding: DataManagerEncoding,
  EventSource,
  Consent,
  ConsentStatus,
  IngestEventsRequest,
  ProductAccount,
  UserData,
  UserIdentifier,
} = protos.google.ads.datamanager.v1;
import {UserDataFormatter, Encoding} from '@google-ads/datamanager-util';
import * as fs from 'fs';
import * as yargs from 'yargs';

const MAX_EVENTS_PER_REQUEST = 10000;

interface Arguments {
  operating_account_type: string;
  operating_account_id: string;
  conversion_action_id: string;
  json_file: string;
  validate_only: boolean;
  login_account_type?: string;
  login_account_id?: string;
  linked_account_type?: string;
  linked_account_id?: string;
  [x: string]: unknown;
}

interface EventRow {
  timestamp: string;
  transactionId: string;
  eventSource?: string;
  gclid?: string;
  currency?: string;
  value?: number;
  emails?: string[];
  phoneNumbers?: string[];
}

/**
 * The main function for the IngestEvents sample.
 */
async function main() {
  const argv: Arguments = yargs
    .option('operating_account_type', {
      describe: 'The account type of the operating account.',
      type: 'string',
      required: true,
    })
    .option('operating_account_id', {
      describe: 'The ID of the operating account.',
      type: 'string',
      required: true,
    })
    .option('conversion_action_id', {
      describe: 'The ID of the conversion action.',
      type: 'string',
      required: true,
    })
    .option('json_file', {
      describe: 'JSON file containing user data to ingest.',
      type: 'string',
      required: true,
    })
    .option('validate_only', {
      describe: 'Whether to enable validate_only on the request.',
      type: 'boolean',
      default: true,
    })
    .option('login_account_type', {
      describe: 'The account type of the login account.',
      type: 'string',
    })
    .option('login_account_id', {
      describe: 'The ID of the login account.',
      type: 'string',
    })
    .option('linked_account_type', {
      describe: 'The account type of the linked account.',
      type: 'string',
    })
    .option('linked_account_id', {
      describe: 'The ID of the linked account.',
      type: 'string',
    })
    .option('config', {
      describe: 'Path to a JSON file with arguments.',
      type: 'string',
    })
    .config('config')
    .check((args: Arguments) => {
      if (
        (args.login_account_type && !args.login_account_id) ||
        (!args.login_account_type && args.login_account_id)
      ) {
        throw new Error(
          'Must specify either both or neither of login account type ' +
            'and login account ID',
        );
      }
      if (
        (args.linked_account_type && !args.linked_account_id) ||
        (!args.linked_account_type && args.linked_account_id)
      ) {
        throw new Error(
          'Must specify either both or neither of linked account type ' +
            'and linked account ID',
        );
      }
      return true;
    })
    .parseSync();

  // Reads event data from the JSON file.
  const eventRows: EventRow[] = readEventDataFile(argv.json_file);

  // Builds the events collection for the request.
  const events = [];
  const formatter = new UserDataFormatter();
  for (const eventRow of eventRows) {
    const event = DataManagerEvent.create();
    try {
      const date = new Date(eventRow.timestamp);
      event.eventTimestamp = {
        seconds: Math.floor(date.getTime() / 1000),
        nanos: (date.getTime() % 1000) * 1e6,
      };
    } catch (e) {
      console.warn(
        `Invalid timestamp format: ${eventRow.timestamp}. Skipping row.`,
      );
      continue;
    }

    if (!eventRow.transactionId) {
      console.warn('Skipping event with no transaction ID');
      continue;
    }
    event.transactionId = eventRow.transactionId;

    if (eventRow.eventSource) {
      const eventSourceEnumValue: number | undefined =
        EventSource[eventRow.eventSource as keyof typeof EventSource];
      if (eventSourceEnumValue === undefined) {
        console.warn(
          `Skipping event with invalid event_source: ${eventRow.eventSource}`,
        );
        continue;
      }
      event.eventSource = eventSourceEnumValue;
    }

    if (eventRow.gclid) {
      event.adIdentifiers = {gclid: eventRow.gclid};
    }

    if (eventRow.currency) {
      event.currency = eventRow.currency;
    }

    if (eventRow.value) {
      event.conversionValue = eventRow.value;
    }

    const userData = UserData.create();
    // Adds a UserIdentifier for each valid email address for the eventRecord.
    if (eventRow.emails) {
      for (const email of eventRow.emails) {
        try {
          const processedEmail = formatter.processEmailAddress(
            email,
            Encoding.HEX,
          );
          userData.userIdentifiers.push(
            UserIdentifier.create({emailAddress: processedEmail}),
          );
        } catch (e) {
          console.warn(`Invalid email address: ${email}. Skipping.`);
        }
      }
    }

    // Adds a UserIdentifier for each valid phone number for the eventRecord.
    if (eventRow.phoneNumbers) {
      for (const phoneNumber of eventRow.phoneNumbers) {
        try {
          const processedPhone = formatter.processPhoneNumber(
            phoneNumber,
            Encoding.HEX,
          );
          userData.userIdentifiers.push(
            UserIdentifier.create({phoneNumber: processedPhone}),
          );
        } catch (e) {
          console.warn(`Invalid phone: ${phoneNumber}. Skipping.`);
        }
      }
    }

    if (userData.userIdentifiers.length > 0) {
      event.userData = userData;
    }

    events.push(event);
  }

  // Sets up the Destination.
  const operatingAccountType = convertToAccountType(
    argv.operating_account_type,
    'operating_account_type',
  );

  const destination = Destination.create({
    operatingAccount: ProductAccount.create({
      accountType: operatingAccountType,
      accountId: argv.operating_account_id,
    }),
    productDestinationId: argv.conversion_action_id,
  });

  // The login account is optional.
  if (argv.login_account_type) {
    const loginAccountType = convertToAccountType(
      argv.login_account_type,
      'login_account_type',
    );
    destination.loginAccount = ProductAccount.create({
      accountType: loginAccountType,
      accountId: argv.login_account_id,
    });
  }

  // The linked account is optional.
  if (argv.linked_account_type) {
    const linkedAccountType = convertToAccountType(
      argv.linked_account_type,
      'linked_account_type',
    );
    destination.linkedAccount = ProductAccount.create({
      accountType: linkedAccountType,
      accountId: argv.linked_account_id,
    });
  }

  const client = new IngestionServiceClient();

  let requestCount = 0;
  // Batches requests to send up to the maximum number of events per request.
  for (let i = 0; i < events.length; i += MAX_EVENTS_PER_REQUEST) {
    requestCount++;
    const eventsBatch = events.slice(i, i + MAX_EVENTS_PER_REQUEST);

    // Builds the request.
    const request = IngestEventsRequest.create({
      destinations: [destination],
      // Adds events from the current batch.
      events: eventsBatch,
      consent: Consent.create({
        adUserData: ConsentStatus.CONSENT_GRANTED,
        adPersonalization: ConsentStatus.CONSENT_GRANTED,
      }),
      // Sets encoding to match the encoding used.
      encoding: DataManagerEncoding.HEX,
      // Sets validate_only. If true, then the Data Manager API only validates the request
      validateOnly: argv.validate_only,
    });

    const [response] = await client.ingestEvents(request);
    console.log(`Response for request #${requestCount}:\n`, response);

    if (response.fieldWarnings && response.fieldWarnings.length > 0) {
      console.warn(
        'Request ingested successfully, but field warnings were returned. ' +
          'Review warning details and update your implementation as needed.',
      );
    }
  }
  console.log(`# of requests sent: ${requestCount}`);
}

/**
 * Reads the event data from the given JSON file.
 * @param {string} jsonFile The path to the JSON file.
 * @return {EventRow[]} An array of event data.
 */
function readEventDataFile(jsonFile: string): EventRow[] {
  const fileContent = fs.readFileSync(jsonFile, 'utf8');
  return JSON.parse(fileContent);
}

/**
 * Validates that a given string is an enum value for the AccountType enum, and
 * if validation passes, returns the AccountType enum value.
 * @param proposedValue the name of an AccountType enum value
 * @param paramName the name of the parameter to use in the error message if validation fails
 * @returns {protos.google.ads.datamanager.v1.ProductAccount.AccountType} The corresponding enum value.
 * @throws {Error} If the string is not an AccountType enum value.
 */
function convertToAccountType(
  proposedValue: string,
  paramName: string,
): protos.google.ads.datamanager.v1.ProductAccount.AccountType {
  const AccountType = ProductAccount.AccountType;
  const accountTypeEnumNames = Object.keys(AccountType).filter(key =>
    isNaN(Number(key)),
  );
  if (!accountTypeEnumNames.includes(proposedValue)) {
    throw new Error(`Invalid ${paramName}: ${proposedValue}`);
  }
  return AccountType[proposedValue as keyof typeof AccountType];
}

if (require.main === module) {
  main().catch(console.error);
}
```

### PHP

```php
<?php
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * Sample of sending an IngestEventsRequest without encryption.
 */

require_once dirname(__DIR__, 1) . '/vendor/autoload.php';

use Google\Ads\DataManager\V1\AdIdentifiers;
use Google\Ads\DataManager\V1\Client\IngestionServiceClient;
use Google\Ads\DataManager\V1\Consent;
use Google\Ads\DataManager\V1\ConsentStatus;
use Google\Ads\DataManager\V1\Destination;
use Google\Ads\DataManager\V1\Encoding as DataManagerEncoding;
use Google\Ads\DataManager\V1\Event;
use Google\Ads\DataManager\V1\EventSource;
use Google\Ads\DataManager\V1\IngestEventsRequest;
use Google\Ads\DataManager\V1\ProductAccount;
use Google\Ads\DataManager\V1\ProductAccount\AccountType;
use Google\Ads\DataManager\V1\UserData;
use Google\Ads\DataManager\V1\UserIdentifier;
use Google\Ads\DataManagerUtil\Encoding;
use Google\Ads\DataManagerUtil\Formatter;
use Google\ApiCore\ApiException;
use Google\Protobuf\Timestamp;

// The maximum number of events allowed per request.
const MAX_EVENTS_PER_REQUEST = 2000;

/**
 * Reads the JSON-formatted event data file.
 *
 * @param string $jsonFile The event data file.
 * @return array A list of associative arrays, each representing an event.
 */
function readEventDataFile(string $jsonFile): array
{
    $jsonContent = file_get_contents($jsonFile);
    if ($jsonContent === false) {
        throw new \RuntimeException(sprintf('Could not read JSON file: %s', $jsonFile));
    }
    $events = json_decode($jsonContent, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new \RuntimeException(sprintf('Invalid JSON in file: %s', $jsonFile));
    }
    return $events;
}

/**
 * Runs the sample.
 *
 * @param int $operatingAccountType The account type of the operating account.
 * @param string $operatingAccountId The ID of the operating account.
 * @param string $conversionActionId The ID of the conversion action.
 * @param string $jsonFile The JSON file containing event data.
 * @param bool $validateOnly Whether to enable validateOnly on the request.
 * @param int|null $loginAccountType The account type of the login account.
 * @param string|null $loginAccountId The ID of the login account.
 * @param int|null $linkedAccountType The account type of the linked account.
 * @param string|null $linkedAccountId The ID of the linked account.
 */
function main(
    int $operatingAccountType,
    string $operatingAccountId,
    string $conversionActionId,
    string $jsonFile,
    bool $validateOnly,
    ?int $loginAccountType = null,
    ?string $loginAccountId = null,
    ?int $linkedAccountType = null,
    ?string $linkedAccountId = null
): void {
    // Reads event data from the data file.
    $eventRecords = readEventDataFile($jsonFile);

    // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
    $formatter = new Formatter();

    // Builds the events collection for the request.
    $events = [];
    foreach ($eventRecords as $eventRecord) {
        $event = new Event();

        if (empty($eventRecord['timestamp'])) {
            error_log('Skipping event with no timestamp.');
            continue;
        }
        try {
            $dateTime = new DateTime($eventRecord['timestamp']);
            $timestamp = new Timestamp();
            $timestamp->fromDateTime($dateTime);
            $event->setEventTimestamp($timestamp);
        } catch (\Exception $e) {
            error_log(sprintf('Skipping event with invalid timestamp: %s', $eventRecord['timestamp']));
            continue;
        }

        if (empty($eventRecord['transactionId'])) {
            error_log('Skipping event with no transaction ID');
            continue;
        }
        $event->setTransactionId($eventRecord['transactionId']);

        if (!empty($eventRecord['eventSource'])) {
            try {
                $event->setEventSource(EventSource::value($eventRecord['eventSource']));
            } catch (\UnexpectedValueException $e) {
                error_log('Skipping event with invalid event source: ' . $eventRecord['eventSource']);
                continue;
            }
        }

        if (!empty($eventRecord['gclid'])) {
            $event->setAdIdentifiers((new AdIdentifiers())->setGclid($eventRecord['gclid']));
        }

        if (!empty($eventRecord['currency'])) {
            $event->setCurrency($eventRecord['currency']);
        }

        if (isset($eventRecord['value'])) {
            $event->setConversionValue($eventRecord['value']);
        }

        $userData = new UserData();
        $identifiers = [];

        if (!empty($eventRecord['emails'])) {
            foreach ($eventRecord['emails'] as $email) {
                try {
                    $preparedEmail = $formatter->processEmailAddress($email, Encoding::Hex);
                    $identifiers[] = (new UserIdentifier())->setEmailAddress($preparedEmail);
                } catch (\InvalidArgumentException $e) {
                    // Skips invalid input.
                    error_log(sprintf('Skipping invalid email: %s', $e->getMessage()));
                    continue;
                }
            }
        }

        if (!empty($eventRecord['phoneNumbers'])) {
            foreach ($eventRecord['phoneNumbers'] as $phoneNumber) {
                try {
                    $preparedPhoneNumber = $formatter->processPhoneNumber($phoneNumber, Encoding::Hex);
                    $identifiers[] = (new UserIdentifier())->setPhoneNumber($preparedPhoneNumber);
                } catch (\InvalidArgumentException $e) {
                    // Skips invalid input.
                    error_log(sprintf('Skipping invalid phone number: %s', $e->getMessage()));
                    continue;
                }
            }
        }

        if (!empty($identifiers)) {
            $userData->setUserIdentifiers($identifiers);
            $event->setUserData($userData);
        }
        $events[] = $event;
    }

    // Builds the destination for the request.
    $destination = (new Destination())
        ->setOperatingAccount((new ProductAccount())
            ->setAccountType($operatingAccountType)
            ->setAccountId($operatingAccountId))
        ->setProductDestinationId($conversionActionId);

    if ($loginAccountType !== null && $loginAccountId !== null) {
        $destination->setLoginAccount((new ProductAccount())
            ->setAccountType($loginAccountType)
            ->setAccountId($loginAccountId));
    }

    if ($linkedAccountType !== null && $linkedAccountId !== null) {
        $destination->setLinkedAccount((new ProductAccount())
            ->setAccountType($linkedAccountType)
            ->setAccountId($linkedAccountId));
    }

    $client = new IngestionServiceClient();
    try {
        $requestCount = 0;
        // Batches requests to send up to the maximum number of events per request.
        foreach (array_chunk($events, MAX_EVENTS_PER_REQUEST) as $eventsBatch) {
            $requestCount++;
            // Builds the request.
            $request = (new IngestEventsRequest())
                ->setDestinations([$destination])
                ->setEvents($eventsBatch)
                ->setConsent((new Consent())
                    ->setAdUserData(ConsentStatus::CONSENT_GRANTED)
                    ->setAdPersonalization(ConsentStatus::CONSENT_GRANTED)
                )
                ->setValidateOnly($validateOnly)
                ->setEncoding(DataManagerEncoding::HEX);

            echo "Request:\n" . json_encode(json_decode($request->serializeToJsonString()), JSON_PRETTY_PRINT) . "\n";
            $response = $client->ingestEvents($request);
            echo "Response for request #{$requestCount}:\n" . json_encode(json_decode($response->serializeToJsonString()), JSON_PRETTY_PRINT) . "\n";

            if (count($response->getFieldWarnings()) > 0) {
                echo 'Request ingested successfully, but field warnings were returned. '
                    . "Review warning details and update your implementation as needed.\n";
            }
        }
        echo "# of requests sent: {$requestCount}\n";
    } catch (ApiException $e) {
        echo 'Error sending request: ' . $e->getMessage() . "\n";
    } finally {
        $client->close();
    }
}

// Command-line argument parsing
$options = getopt(
    '',
    [
        'operating_account_type:',
        'operating_account_id:',
        'login_account_type::',
        'login_account_id::',
        'linked_account_type::',
        'linked_account_id::',
        'conversion_action_id:',
        'json_file:',
        'validate_only::'
    ]
);

$operatingAccountType = $options['operating_account_type'] ?? null;
$operatingAccountId = $options['operating_account_id'] ?? null;
$conversionActionId = $options['conversion_action_id'] ?? null;
$jsonFile = $options['json_file'] ?? null;

// Only validates requests by default.
$validateOnly = true;
if (array_key_exists('validate_only', $options)) {
    $value = $options['validate_only'];
    // `getopt` with `::` returns boolean `false` if the option is passed without a value.
    if ($value === false || !in_array($value, ['true', 'false'], true)) {
        echo "Error: --validate_only requires a value of 'true' or 'false'.\n";
        exit(1);
    }
    $validateOnly = ($value === 'true');
}

if (empty($operatingAccountType) || empty($operatingAccountId) || empty($conversionActionId) || empty($jsonFile)) {
    echo 'Usage: php ingest_events.php ' .
        '--operating_account_type=<account_type> ' .
        '--operating_account_id=<account_id> ' .
        '--conversion_action_id=<conversion_action_id> ' .
        "--json_file=<path_to_json>\n" .
        'Optional: --login_account_type=<account_type> --login_account_id=<account_id> ' .
        '--linked_account_type=<account_type> --linked_account_id=<account_id> ' .
        "--validate_only=<true|false>\n";
    exit(1);
}

// Converts the operating account type string to an AccountType enum.
$parsedOperatingAccountType = AccountType::value($operatingAccountType);

if (isset($options['login_account_type']) != isset($options['login_account_id'])) {
    throw new \InvalidArgumentException(
        'Must specify either both or neither of login account type and login account ID'
    );
}

$parsedLoginAccountType = null;
if (isset($options['login_account_type'])) {
    // Converts the login account type string to an AccountType enum.
    $parsedLoginAccountType = AccountType::value($options['login_account_type']);
}

if (isset($options['linked_account_type']) != isset($options['linked_account_id'])) {
    throw new \InvalidArgumentException(
        'Must specify either both or neither of linked account type and linked account ID'
    );
}

$parsedLinkedAccountType = null;
if (isset($options['linked_account_type'])) {
    // Converts the linked account type string to an AccountType enum.
    $parsedLinkedAccountType = AccountType::value($options['linked_account_type']);
}

main(
    $parsedOperatingAccountType,
    $operatingAccountId,
    $conversionActionId,
    $jsonFile,
    $validateOnly,
    $parsedLoginAccountType,
    $options['login_account_id'] ?? null,
    $parsedLinkedAccountType,
    $options['linked_account_id'] ?? null
);
```

### Python

```python
#!/usr/bin/env python
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sample of sending an IngestEventsRequest without encryption."""

import argparse
import json
import logging
from typing import Any, Dict, List, Optional

from google.ads import datamanager_v1
from google.ads.datamanager_util import Formatter
from google.ads.datamanager_util.format import Encoding
from google.protobuf.timestamp_pb2 import Timestamp

_logger = logging.getLogger(__name__)

# The maximum number of events allowed per request.
_MAX_EVENTS_PER_REQUEST = 10_000


def main(
    operating_account_type: datamanager_v1.ProductAccount.AccountType,
    operating_account_id: str,
    conversion_action_id: str,
    json_file: str,
    validate_only: bool,
    login_account_type: Optional[
        datamanager_v1.ProductAccount.AccountType
    ] = None,
    login_account_id: Optional[str] = None,
    linked_account_type: Optional[
        datamanager_v1.ProductAccount.AccountType
    ] = None,
    linked_account_id: Optional[str] = None,
) -> None:
    """Runs the sample.
    Args:
     operating_account_type: the account type of the operating account.
     operating_account_id: the ID of the operating account.
     json_file: the JSON file containing event data.
     validate_only: whether to enable validate_only on the request.
     login_account_type: the account type of the login account.
     login_account_id: the ID of the login account.
     linked_account_type: the account type of the linked account.
     linked_account_id: the ID of the linked account.
    """

    # Gets an instance of the formatter.
    formatter: Formatter = Formatter()

    # Reads the input file.
    event_rows: List[Dict[str, Any]] = read_event_data_file(json_file)
    events: List[datamanager_v1.Event] = []
    for event_row in event_rows:
        event = datamanager_v1.Event()
        try:
            event_timestamp = Timestamp()
            event_timestamp.FromJsonString(str(event_row["timestamp"]))
            event.event_timestamp = event_timestamp
        except ValueError:
            _logger.warning(
                "Invalid timestamp format: %s. Skipping row.",
                event_row["timestamp"],
            )
            continue

        if "transactionId" not in event_row:
            _logger.warning("Skipping event with no transaction ID")
            continue
        event.transaction_id = event_row["transactionId"]

        if "eventSource" in event_row:
            event.event_source = event_row["eventSource"]

        if "gclid" in event_row:
            event.ad_identifiers = datamanager_v1.AdIdentifiers(
                gclid=event_row["gclid"]
            )

        if "currency" in event_row:
            event.currency = event_row["currency"]

        if "value" in event_row:
            event.conversion_value = event_row["value"]

        user_data = datamanager_v1.UserData()
        # Adds a UserIdentifier for each valid email address for the event row.
        if "emails" in event_row:
            for email in event_row["emails"]:
                try:
                    processed_email: str = formatter.process_email_address(
                        email, Encoding.HEX
                    )
                    user_data.user_identifiers.append(
                        datamanager_v1.UserIdentifier(
                            email_address=processed_email
                        )
                    )
                except ValueError:
                    # Skips invalid input.
                    _logger.warning(
                        "Invalid email address: %s. Skipping.",
                        event_row["email_address"],
                    )

        # Adds a UserIdentifier for each valid phone number for the event row.
        if "phoneNumbers" in event_row:
            for phone_number in event_row["phoneNumbers"]:
                try:
                    processed_phone: str = formatter.process_phone_number(
                        phone_number, Encoding.HEX
                    )
                    user_data.user_identifiers.append(
                        datamanager_v1.UserIdentifier(
                            phone_number=processed_phone
                        )
                    )
                except ValueError:
                    # Skips invalid input.
                    _logger.warning(
                        "Invalid phone: %s. Skipping.",
                        event_row["phone_number"],
                    )

        if user_data.user_identifiers:
            event.user_data = user_data

        # Adds the event to the list of events to send in the request.
        events.append(event)

    # Configures the destination.
    destination: datamanager_v1.Destination = datamanager_v1.Destination()
    destination.operating_account.account_type = operating_account_type
    destination.operating_account.account_id = operating_account_id
    destination.product_destination_id = str(conversion_action_id)
    if login_account_type or login_account_id:
        if bool(login_account_type) != bool(login_account_id):
            raise ValueError(
                "Must specify either both or neither of login "
                + "account type and login account ID"
            )
        destination.login_account.account_type = login_account_type
        destination.login_account.account_id = login_account_id
    if linked_account_type or linked_account_id:
        if bool(linked_account_type) != bool(linked_account_id):
            raise ValueError(
                "Must specify either both or neither of linked account "
                + "type and linked account ID"
            )
        destination.linked_account.account_type = linked_account_type
        destination.linked_account.account_id = linked_account_id

    # Creates a client for the ingestion service.
    client: datamanager_v1.IngestionServiceClient = (
        datamanager_v1.IngestionServiceClient()
    )

    # Batches requests to send up to the maximum number of events per
    # request.
    request_count = 0
    for i in range(0, len(events), _MAX_EVENTS_PER_REQUEST):
        request_count += 1
        events_batch = events[i : i + _MAX_EVENTS_PER_REQUEST]
        # Sends the request.
        request: datamanager_v1.IngestEventsRequest = (
            datamanager_v1.IngestEventsRequest(
                destinations=[destination],
                # Adds events from the current batch.
                events=events_batch,
                consent=datamanager_v1.Consent(
                    ad_user_data=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                    ad_personalization=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                ),
                # Sets encoding to match the encoding used.
                encoding=datamanager_v1.Encoding.HEX,
                # Sets validate_only. If true, then the Data Manager API only
                # validates the request but doesn't apply changes.
                validate_only=validate_only,
            )
        )

        # Sends the request.
        response: datamanager_v1.IngestEventsResponse = client.ingest_events(
            request=request
        )

        # Logs the response.
        _logger.info("Response for request #%d:\n%s", request_count, response)

        if response.field_warnings:
            _logger.warning(
                "Request ingested successfully, but field warnings were returned. "
                "Review warning details and update your implementation as needed."
            )

    _logger.info("# of requests sent: %d", request_count)


def read_event_data_file(json_file: str) -> List[Dict[str, Any]]:
    """Reads the JSON-formatted event data file.
    Args:
      json_file: the event data file.
    """
    with open(json_file, "r") as f:
        return json.load(f)


if __name__ == "__main__":
    # Configures logging.
    logging.basicConfig(level=logging.INFO)

    parser = argparse.ArgumentParser(
        description=("Sends events from a JSON file to a destination."),
        fromfile_prefix_chars="@",
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "--operating_account_type",
        type=str,
        required=True,
        help="The account type of the operating account.",
    )
    parser.add_argument(
        "--operating_account_id",
        type=str,
        required=True,
        help="The ID of the operating account.",
    )
    parser.add_argument(
        "--conversion_action_id",
        type=int,
        required=True,
        help="The ID of the conversion action",
    )
    parser.add_argument(
        "--login_account_type",
        type=str,
        required=False,
        help="The account type of the login account.",
    )
    parser.add_argument(
        "--login_account_id",
        type=str,
        required=False,
        help="The ID of the login account.",
    )
    parser.add_argument(
        "--linked_account_type",
        type=str,
        required=False,
        help="The account type of the linked account.",
    )
    parser.add_argument(
        "--linked_account_id",
        type=str,
        required=False,
        help="The ID of the linked account.",
    )
    parser.add_argument(
        "--json_file",
        type=str,
        required=True,
        help="JSON file containing user data to ingest.",
    )
    parser.add_argument(
        "--validate_only",
        choices=["true", "false"],
        default="true",
        help="""Whether to enable validate_only on the request. Must be
        'true' or 'false'. Defaults to 'true'.""",
    )
    args = parser.parse_args()

    main(
        args.operating_account_type,
        args.operating_account_id,
        args.conversion_action_id,
        args.json_file,
        args.validate_only == "true",
        args.login_account_type,
        args.login_account_id,
        args.linked_account_type,
        args.linked_account_id,
    )
```

## Success responses

A successful request returns a response with an object containing a `requestId`.
If any optional fields failed validation, the response also includes a
[`fieldWarnings`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#body.IngestEventsResponse.FIELDS.field_warnings)
list.

### Standard response

Here is a sample response for a successful ingestion request with no warnings:

    {
      "requestId": "126365e1-16d0-4c81-9de9-f362711e250a"
    }

### Response with warnings

Here is a sample response for a successful ingestion request that contains a
warning:

    {
      "requestId": "126365e1-16d0-4c81-9de9-f362711e250a",
      "fieldWarnings": [
        {
          "field": "events.events[0].cart_data.items[0].merchant_product_id",
          "description": "The merchant product ID is missing in the cart item.",
          "reason": "WARNING_REASON_CART_DATA_ITEM_MERCHANT_PRODUCT_ID_MISSING"
        }
      ]
    }

Record the `requestId` returned so you can retrieve
[diagnostics](https://developers.google.com/data-manager/api/devguides/diagnostics) as each destination in the request is
processed. You should also check for any
[`fieldWarnings`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#body.IngestEventsResponse.FIELDS.field_warnings)
to ensure any optional fields you sent were accepted. See [Ingestion
warnings](https://developers.google.com/data-manager/api/devguides/concepts/understand-errors#ingestion-warnings) for more
details.

> [!NOTE]
> **Note:** You can only retrieve diagnostics for requests that succeed and don't have `validateOnly` set to `true`.

## Failure responses

A failed request results in an error response status code such as `400 Bad
Request`, and a response with error details.

For example, an `emailAddress` containing a plain text string instead of a hex
encoded value produces the following response:

    {
      "error": {
        "code": 400,
        "message": "There was a problem with the request.",
        "status": "INVALID_ARGUMENT",
        "details": [
          {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "INVALID_ARGUMENT",
            "domain": "datamanager.googleapis.com"
          },
          {
            "@type": "type.googleapis.com/google.rpc.BadRequest",
            "fieldViolations": [
              {
                "field": "events.events[0].user_data.user_identifiers",
                "description": "Email is not hex encoded.",
                "reason": "INVALID_HEX_ENCODING"
              }
            ]
          }
        ]
      }
    }

An `emailAddress` that isn't hashed and is only hex encoded produces the
following response:

    {
      "error": {
        "code": 400,
        "message": "There was a problem with the request.",
        "status": "INVALID_ARGUMENT",
        "details": [
          {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "INVALID_ARGUMENT",
            "domain": "datamanager.googleapis.com"
          },
          {
            "@type": "type.googleapis.com/google.rpc.BadRequest",
            "fieldViolations": [
              {
                "field": "events.events[0]",
                "reason": "INVALID_SHA256_FORMAT"
              }
            ]
          }
        ]
      }
    }

## Send events for multiple destinations

If your data contains events for different destinations, you can send them in
the same request by using destination references. See [Limits and
quotas](https://developers.google.com/data-manager/api/devguides/limits#request_limits) for the maximum
number of destinations per request.

For example, if you have an event for conversion action ID `123456789` and
another event for conversion action ID `777111122`, send both events in a single
request by setting the `reference` of each
[`Destination`](https://developers.google.com/data-manager/api/reference/rest/v1/Destination). The `reference` is
user-defined. The only requirement is that each `Destination` has a unique
`reference`. Here's the modified `destinations` list for the request:

### Advertiser

      "destinations": [
        {
          "operatingAccount": {
            "accountType": "OPERATING_ACCOUNT_TYPE",
            "accountId": "OPERATING_ACCOUNT_ID"
          },
          "loginAccount": {
            "accountType": "LOGIN_ACCOUNT_TYPE",
            "accountId": "LOGIN_ACCOUNT_ID"
          },
          "productDestinationId": "123456789",
          "reference": "destination_a"
        },
        {
          "operatingAccount": {
            "accountType": "OPERATING_ACCOUNT_2_TYPE",
            "accountId": "OPERATING_ACCOUNT_2_ID"
          },
          "loginAccount": {
            "accountType": "LOGIN_ACCOUNT_2_TYPE",
            "accountId": "LOGIN_ACCOUNT_2_ID"
          },
          "productDestinationId": "777111122",
          "reference": "destination_b"
        }
      ]

### Data Partner

      "destinations": [
        {
          "operatingAccount": {
            "accountType": "OPERATING_ACCOUNT_TYPE",
            "accountId": "OPERATING_ACCOUNT_ID"
          },
          "loginAccount": {
            "accountType": "DATA_PARTNER",
            "accountId": "DATA_PARTNER_ACCOUNT_ID"
          },
          "linkedAccount": {
            "accountType": "LINKED_ACCOUNT_TYPE",
            "accountId": "LINKED_ACCOUNT_ID"
          },
          "productDestinationId": "123456789",
          "reference": "destination_a"
        },
        {
          "operatingAccount": {
            "accountType": "OPERATING_ACCOUNT_2_TYPE",
            "accountId": "OPERATING_ACCOUNT_2_ID"
          },
          "loginAccount": {
            "accountType": "DATA_PARTNER",
            "accountId": "DATA_PARTNER_ACCOUNT_2_ID"
          },
          "linkedAccount": {
            "accountType": "LINKED_ACCOUNT_2_TYPE",
            "accountId": "LINKED_ACCOUNT_2_ID"
          },
          "productDestinationId": "777111122",
          "reference": "destination_b"
        }
      ]

Set the `destinationReferences` of each
[`Event`](https://developers.google.com/data-manager/api/reference/rest/v1/events/ingest#event) to send it to one or more
specific destinations. For example, here's an `Event` that's only for the first
`Destination`, so its `destinationReferences` list only contains the
`reference` of the first `Destination`:

    {
       "adIdentifiers": {
          "gclid": "GCLID_1"
       },
       "conversionValue": 1.99,
       "currency": "USD",
       "eventTimestamp": "2025-06-10T20:07:01Z",
       "transactionId": "ABC798654321",
       "eventSource": "WEB",
       "destinationReferences": [
          "destination_a"
       ]
    }

The `destinationReferences` field is a list, so you can specify multiple
destinations for an event. If you don't set the `destinationReferences` of an
`Event`, the Data Manager API sends the event to all of the destinations in the
request.

If an event has multiple destinations, the Data Manager API sends relevant fields to
each destination. For example, if an event has a Google Ads destination and a
Google Analytics destination, the API includes Google Analytics fields such as `clientId`,
`appInstanceId`, or `eventName` when sending the event to the Google Analytics
destination, and includes Google Ads fields such as `customVariables` when sending
the event to the Google Ads destination.

## Next steps

- [Configure](https://developers.google.com/data-manager/api/devguides/quickstart/set-up-access) authentication and setup your environment with a client library.
- Learn about the [formatting, hashing, and encoding requirements](https://developers.google.com/data-manager/api/devguides/concepts/formatting) for each type of data.
- Learn how to [encrypt user data](https://developers.google.com/data-manager/api/devguides/concepts/encryption).
- Learn how to [retrieve diagnostics](https://developers.google.com/data-manager/api/devguides/diagnostics) for your requests.
- Learn about [best practices](https://developers.google.com/data-manager/api/devguides/concepts/best-practices).
- Learn about [limits and quotas](https://developers.google.com/data-manager/api/devguides/limits).