Enviar eventos

Puedes completar esta guía de inicio rápido para familiarizarte con el envío de datos de eventos.

Usa la API de Data Manager en cualquiera de las siguientes situaciones:

  • Envía las conversiones de la etiqueta de Google Ads o los eventos de purchase de Google Analytics como una fuente de datos adicional para tus conversiones de etiquetas, de modo que puedas maximizar los indicadores de interacción con los anuncios y fortalecer tus datos y tu rendimiento general.

    Esta función está disponible para todas las cuentas de Google Ads, pero solo para las propiedades de Google Analytics que se encuentran en una lista de entidades permitidas. Completa el formulario si te interesa agregar tu propiedad de Google Analytics a la lista de entidades permitidas.

  • Envía datos de eventos para las conversiones sin conexión de Google Ads o las conversiones avanzadas de clientes potenciales.

Elige la versión de la guía que deseas ver:

En esta guía de inicio rápido, completarás los siguientes pasos:

  1. Prepara un objeto Destination para recibir datos de eventos.
  2. Prepara los datos de eventos para enviarlos.
  3. Compila una solicitud de IngestionService para eventos.
  4. Envía la solicitud con el Explorador de APIs de Google.
  5. Comprende las respuestas de éxito y error.

Prepara destinos

Antes de enviar datos, debes preparar al menos un Destination para los datos. Aquí tienes un ejemplo de Destination que puedes usar:

    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_TYPE",
        "accountId": "OPERATING_ACCOUNT_ID"
      },

      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_TYPE",
        "accountId": "LOGIN_ACCOUNT_ID"
      },

      "productDestinationId": "PRODUCT_DESTINATION_ID"
    }

Estos son los campos de un Destination. Consulta Configura destinos para obtener más detalles y ejemplos de destinos para diferentes situaciones.

operatingAccount

Es la cuenta que recibe los eventos.

En el caso de los eventos enviados como una fuente de datos adicional, la cuenta operativa puede ser una cuenta de Google Ads o una propiedad de Google Analytics.

Si accountType es GOOGLE_ANALYTICS_PROPERTY, las credenciales de la solicitud deben ser para un usuario de Google Analytics con el rol de editor o administrador para la propiedad.

En el caso de las conversiones sin conexión y las conversiones avanzadas de clientes potenciales, la cuenta operativa debe ser una cuenta de Google Ads.

loginAccount
Es la cuenta en la que la Cuenta de Google para las credenciales es un usuario.
productDestinationId

Es el ID de la entidad en el operatingAccount que recibe los eventos.

En el caso de los eventos enviados como una fuente de datos adicional, el productDestinationId debe ser uno de los siguientes:

  1. Es el ID de una conversión de Google Ads con type establecido en WEBPAGE. En la IU de Google Ads, la Fuente de conversión de una acción de conversión de WEBPAGE es Sitio web.

  2. Es el ID de medición de un flujo web de Google Analytics. No puedes enviar eventos como una fuente de datos adicional a un flujo de datos de aplicación para iOS o Android de Google Analytics.

En el caso de las conversiones sin conexión o las conversiones avanzadas de clientes potenciales, el elemento productDestinationId debe ser el ID de una acción de conversión de Google Ads con type configurado como UPLOAD_CLICKS. En la IU de Google Ads, la Fuente de conversión de una acción de conversión de UPLOAD_CLICKS es Sitio web (importación desde clics).

En el ejemplo de esta guía, se muestra cómo crear una solicitud que envíe todos los eventos al mismo destino. Si deseas enviar eventos para varios destinos en la misma solicitud, consulta Cómo enviar eventos para varios destinos.

Prepara los datos de eventos

Considera los siguientes datos de eventos. Cada tabla corresponde a un evento de conversión. Cada evento de conversión tiene una marca de tiempo, su acción de conversión y su valor de conversión.

Cada evento puede tener identificadores de publicidad, como gclid, o identificadores de usuario, como direcciones de correo electrónico, números de teléfono y dirección. Un evento también puede tener lo siguiente:

Estos son los datos del evento:

Evento 1

Evento 1
conversion_time 2025-06-10 15:07:01-05:00
conversion_action_id 123456789
transaction_id ABC798654321
conversion_value 30.03
currency USD
gclid GCLID_1
emails
given_name John
family_name Smith-Jones
region_code us
postal_code 94045
customer_type NEW
customer_value_bucket HIGH
client_id 1234567890.1761581763
user_id user_ABC12345
ad_unit_name Banner_01
event_name purchase
Artículos del carrito
item_id SKU_12345
item_name Stan and Friends Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 2.22
item_index 0
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 10.01
item_quantity 3

Evento 2

Evento 2
conversion_time June 10, 2025 11:42:33PM America/New_York
conversion_action_id 123456789
transaction_id DEF999911111
conversion_value 42.02
currency eur
gclid GCLID_2
emails

zoe@EXAMPLE.COM

cloudy.sanfrancisco@gmail.com

given_name zoë
family_name pérez
region_code PT
postal_code 1229-076
customer_type RETURNING
client_id 9876543210.1761582117
user_id user_DEF9876
ad_unit_name Banner_02
event_name purchase
Artículos del carrito
item_id SKU_12346
item_name Google Grey Women's Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 3.33
item_index 1
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 21.01
item_quantity 2

Formatea los datos

Da formato a los campos según se especifica en la guía de formato. Estos son los datos del evento después del formato:

Evento 1

Evento 1
conversion_time 2025-06-10T15:07:01-05:00
conversion_action_id 123456789
transaction_id ABC798654321
conversion_value 30.03
currency USD
gclid GCLID_1
emails
given_name john
family_name smith-jones
region_code US
postal_code 94045
customer_type NEW
customer_value_bucket HIGH
client_id 1234567890.1761581763
user_id user_ABC12345
ad_unit_name Banner_01
event_name purchase
Artículos del carrito
item_id SKU_12345
item_name Stan and Friends Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 2.22
item_index 0
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 10.01
item_quantity 3

Evento 2

Evento 2
conversion_time 2025-06-10T23:42:33-05:00
conversion_action_id 123456789
transaction_id DEF999911111
conversion_value 42.02
currency EUR
gclid GCLID_2
emails

zoe@example.com

cloudysanfrancisco@gmail.com

given_name zoë
family_name pérez
region_code PT
postal_code 1229-076
customer_type RETURNING
client_id 9876543210.1761582117
user_id user_DEF9876
ad_unit_name Banner_02
event_name purchase
Artículos del carrito
item_id SKU_12346
item_name Google Grey Women's Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 3.33
item_index 1
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 21.01
item_quantity 2

Genera un hash y codifica los datos

Además, las direcciones de correo electrónico, los nombres y los apellidos con formato deben tener una codificación hash con el algoritmo SHA-256 y codificarse con codificación hexadecimal o Base64. Estos son los datos del evento después de aplicar formato, codificación hash y codificación hexadecimal:

Evento 1

Evento 1
conversion_time 2025-06-10T15:07:01-05:00
conversion_action_id 123456789
transaction_id ABC798654321
conversion_value 30.03
currency USD
gclid GCLID_1
emails
given_name 96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A
family_name DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081
region_code US
postal_code 94045
customer_type NEW
customer_value_bucket HIGH
client_id 1234567890.1761581763
user_id user_ABC12345
ad_unit_name Banner_01
event_name purchase
Artículos del carrito
item_id SKU_12345
item_name Stan and Friends Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 2.22
item_index 0
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 10.01
item_quantity 3

Evento 2

Evento 2
conversion_time 2025-06-10T23:42:33-05:00
conversion_action_id 123456789
transaction_id DEF999911111
conversion_value 42.02
currency EUR
gclid GCLID_2
emails

3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250

223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4

given_name 2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450
family_name 6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F
region_code PT
postal_code 1229-076
customer_type RETURNING
client_id 9876543210.1761582117
user_id user_DEF9876
ad_unit_name Banner_02
event_name purchase
Artículos del carrito
item_id SKU_12346
item_name Google Grey Women's Tee
item_affiliation Google Merchandise Store
item_coupon SUMMER_FUN
item_discount 3.33
item_index 1
item_brand Google
item_category Apparel
item_category2 Adult
item_category3 Shirts
item_category4 Crew
item_category5 Short sleeve
item_list_id related_products
item_list_name Related Products
item_price 21.01
item_quantity 2

Convierte los datos en objetos Event

Convierte los datos con formato y codificación hash de cada evento en un Event. Propaga los siguientes campos como se indica:

  1. Establece eventTimestamp en la hora en que ocurrió el evento.

    Los eventos de Google Analytics deben tener un eventTimestamp dentro de las últimas 72 horas.

  2. Establece los campos obligatorios para tu caso de uso.

    Caso de uso Identificadores transactionId eventSource
    Conversiones sin conexión o conversiones avanzadas de clientes potenciales Obligatorio. Establece al menos uno de los siguientes parámetros: Opcional Obligatorio. Se establece en uno de los valores de enumeración para EventSource.
    Eventos enviados como una fuente de datos adicional a un destino de Google Ads Obligatorio. Establece al menos uno de los siguientes parámetros: Obligatorio Opcional. Si se configura, debe ser WEB.
    Eventos enviados como una fuente de datos adicional a un destino de Google Analytics Obligatorio. Establece al menos uno de los siguientes parámetros: Obligatorio Opcional. Si se configura, debe ser WEB.
  3. Si envías eventos como una fuente de datos adicional a un destino de Google Ads, consulta Cómo Google maneja los datos de fuentes de datos adicionales.

  4. Propaga cualquier otro campo en el que tengas un valor para el evento. Consulta la documentación de referencia de Event para obtener la lista completa de los campos disponibles.

Cómo Google maneja los datos de fuentes de datos adicionales

Dentro de la misma acción de conversión, Google utiliza el transactionId para quitar los duplicados de los eventos de conversión enviados desde diferentes fuentes (como la etiqueta de tu sitio web y las solicitudes de transferencia de la API de Data Manager). En la siguiente tabla, se explica cómo se procesan los datos de tus solicitudes de transferencia.

Situación Campo de datos Cómo se maneja
transactionId COINCIDE con un evento de etiqueta existente conversionValue (con currencyCode)

Se actualizó. El conversionValue (con currencyCode) del Event anula el valor original que registra la etiqueta.

Nota: Durante el período de prueba inicial de 14 días de una acción de conversión, las actualizaciones de valor están inhabilitadas. El valor de la etiqueta no se anulará en los informes de Google Ads hasta que finalice el período de prueba.

transactionId COINCIDE con un evento de etiqueta existente Otros campos, excepto conversionValue o currencyCode (por ejemplo, adIdentifiers.gclid) Se ignora. Otros valores de campo de tu fuente de datos adicional no reemplazarán los valores de campo que registró originalmente tu etiqueta de Google para las transacciones coincidentes.
transactionId NO coincide con ningún evento existente Todos los datos proporcionados (por ejemplo, userData, conversionValue y currencyCode)

Se usa para crear un evento de conversión nuevo. Luego, Google intentará atribuir esta nueva conversión a un clic en un anuncio con los identificadores que proporcionaste (como adIdentifiers.gclid o userData).

Nota: Durante el período de prueba inicial de 14 días, estas conversiones creadas recientemente aparecerán en tus informes, pero no se utilizarán para establecer ofertas. Una vez que finalice la prueba, automáticamente serán aptas para ofertas.

Agrega atributos de sesión

Si envías conversiones sin conexión o conversiones avanzadas de clientes potenciales, agrega atributos de sesión cuando no estén disponibles otros identificadores de anuncios, como un GCLID o un WBRAID. También puedes incluir atributos de sesión además de otros identificadores de anuncios.

Los atributos de sesión proporcionan más contexto e indicadores sobre la interacción del usuario con tu sitio web, lo que puede mejorar la medición de conversiones, los informes y la precisión de las ofertas.

En la API de Data Manager, puedes usar dos enfoques para enviar atributos de sesión:

  1. Recomendación: Establece el campo sessionAttributes de adIdentifiers en la cadena de atributos de sesión codificada en base64. Sigue las instrucciones que se indican en Cómo capturar session_attributes para modificar las páginas de envío de formularios y capturar la cadena codificada.

  2. Si no puedes usar JavaScript, captura los campos de atributos de sesión individuales y agrega cada uno a la lista experimentalFields como un ExperimentalField independiente:

    • gad_campaignid
    • session_start_time_usec
    • gad_source
    • landing_page_url
    • landing_page_referrer

    Si tienes un valor para el atributo de sesión landing_page_user_agent, envíalo en el campo userAgent de adIdentifiers.landingPageDeviceInfo.

    A continuación, se indican las prácticas recomendadas para enviar pares clave-valor individuales:

    • Envía gad_campaignid y session_start_time_usec de forma coherente. Estos campos son fundamentales para una atribución precisa.
    • No proporciones un valor de landing_page_url inexacto o parcial, como una cadena de marcador de posición, una ruta de acceso interna a la aplicación o una URL incompleta. Omite landing_page_url si no tienes la URL completa y precisa.

    A continuación, se muestra una parte de un evento de muestra con entradas en experimentalFields para gad_campaignid y session_start_time_usec, y el agente de usuario en el campo landingPageDeviceInfo:

    {
      ...,
      "experimentalFields": [
        {
          "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"
        }
      }
    }
    
    

Agrega información de Google Analytics

Si los destinos de un evento enviado como fuente de datos adicional incluyen una propiedad de Google Analytics, completa los siguientes campos como se indica:

eventName

Obligatorio. Es el nombre del evento de Google Analytics.

transactionId

Obligatorio . Es el identificador único del evento.

Al menos un identificador

Se debe configurar al menos uno de los siguientes campos:

destinationReferences

Obligatorio si la lista destinations a nivel de la solicitud contiene más de un Destination de Google Analytics. Agrega una entrada a destinationReferences para especificar qué destino de Google Analytics debe recibir el evento. Consulta cómo enviar eventos a varios destinos para obtener más información sobre las referencias de destino.

Si destinationReferences no está configurado o tiene varias entradas que hacen referencia a destinos de Google Analytics, la API de Data Manager rechaza el evento con el error MULTIPLE_DESTINATIONS_FOR_GOOGLE_ANALYTICS_EVENT.

userId

Opcional. Es el User-ID del usuario.

additionalEventParameters

Opcional, pero recomendado. Propaga esta lista con los parámetros de eventos de Google Analytics que no se capturen en los otros campos de Event. Los parámetros pueden incluir parámetros recomendados adicionales del evento purchase o cualquier otro parámetro que desees capturar. Utiliza el nombre del parámetro de Google Analytics para el parameterName del EventParameter.

Por ejemplo, si tienes los impuestos asociados a una transacción, agrega una entrada a additionalEventParameters con parameterName establecido en tax y value establecido en el costo de impuestos.

No recomendamos agregar entradas para los parámetros de eventos de Google Analytics transactionId, currency o value. En su lugar, completa los campos transactionId, currency y conversionValue del objeto Event, que tienen prioridad sobre cualquier entrada en additionalEventParameters.

Agrega datos del carrito para los eventos de compra

Propaga el campo cartData del objeto Event con información sobre los artículos comprados. Para cada artículo comprado, agrega un objeto Item a la lista items del objeto CartData y completa los siguientes campos como se indica:

itemId
Obligatorio. Es un identificador único del elemento.
unitPrice

Obligatorio. Es el precio por unidad sin impuestos, envío ni descuentos con alcance del evento (a nivel de la transacción).

Si el artículo tiene un descuento aplicado al artículo, usa el precio por unidad con descuento. Por ejemplo, si un artículo tiene un precio por unidad de 27.67 y un descuento por unidad de 6.66, establece unitPrice en 21.01.

quantity

Obligatorio. Es la cantidad de unidades compradas de este artículo en particular.

additionalItemParameters

Completa esta lista con cualquier parámetro con alcance del elemento que no se capture en los otros campos Item. Usa el nombre del parámetro del elemento de Google Analytics para el parameterName del ItemParameter.

Por ejemplo, si tienes la marca y la categoría de un elemento, agrega una entrada al additionalItemParameters del elemento con parameterName establecido en item_brand y value establecido en el nombre de la marca, y otra entrada con parameterName establecido en item_category y value establecido en la categoría del elemento.

No recomendamos agregar entradas para los parámetros de elementos quantity, price o item_id de Google Analytics. En su lugar, completa los campos itemId, unitPrice y quantity del objeto Item, que tienen prioridad sobre cualquier entrada en additionalItemParameters.

A continuación, se muestra un ejemplo de Event para los datos con formato, codificados y con hash del segundo evento, con datos adicionales para Google Analytics:

{
  "adIdentifiers": {
     "gclid": "GCLID_2"
  },
  "conversionValue": 42.02,
  "currency": "EUR",
  "eventTimestamp": "2025-06-10T23:42:33-05:00",
  "transactionId": "DEF999911111",
  "eventSource": "WEB",
  "userData": {
    "userIdentifiers": [
      {
        "emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250"
      },
      {
        "emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4"
      },
      {
        "address": {
          "givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450",
          "familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F",
          "regionCode": "PT",
          "postalCode": "1229-076"
        }
      }
    ],
  },
  "userProperties": {
    "customerType": "RETURNING"
  },
  "eventName": "purchase",
  "clientId": "9876543210.1761582117",
  "userId": "user_DEF9876",
  "additionalEventParameters": [
    {
      "parameterName": "ad_unit_name",
      "value": "Banner_02"
    }
  ],
  "cartData": {
    "transactionDiscount": 6.66,
    "items": [
      {
        "itemId": "SKU_12346",
        "quantity": 2,
        "unitPrice": 21.01,
        "additionalItemParameters": [
          {
            "parameterName": "item_name",
            "value": "Google Grey Women's Tee"
          },
          {
            "parameterName": "affiliation",
            "value": "Google Merchandise Store"
          },
          {
            "parameterName": "coupon",
            "value": "SUMMER_FUN"
          },
          {
            "parameterName": "discount",
            "value": "3.33"
          },
          {
            "parameterName": "index",
            "value": "1"
          },
          {
            "parameterName": "item_brand",
            "value": "Google"
          },
          {
            "parameterName": "item_category",
            "value": "Apparel"
          },
          {
            "parameterName": "item_category2",
            "value": "Adult"
          },
          {
            "parameterName": "item_category3",
            "value": "Shirts"
          },
          {
            "parameterName": "item_category4",
            "value": "Crew"
          },
          {
            "parameterName": "item_category5",
            "value": "Short sleeve"
          },
          {
            "parameterName": "item_list_id",
            "value": "related_products"
          },
          {
            "parameterName": "item_list_name",
            "value": "Related Products"
          }
        ]
      }
    ]
  }
}

Crea el cuerpo de la solicitud

Para crear el cuerpo de la solicitud, combina destinations y events, configura el campo encoding y agrega cualquier otro campo de solicitud que desees incluir, como validateOnly y consent.

Los ejemplos de esta guía no usan encriptación, pero puedes seguir las instrucciones en Encripta los datos del usuario para agregar encriptación a tu proceso.

Envía la solicitud

Sigue estos pasos para probar una solicitud desde tu navegador:

  1. Selecciona la pestaña REST y haz clic en Abrir en API Explorer para abrir API Explorer en una nueva pestaña o ventana.
  2. En el cuerpo de la solicitud del Explorador de APIs, reemplaza cada cadena que comience con REPLACE_WITH, como REPLACE_WITH_OPERATING_ACCOUNT_TYPE, por el valor pertinente.
  3. Haz clic en Ejecutar en la parte inferior de la página del Explorador de APIs y completa las indicaciones de autorización para enviar la solicitud.
  4. Establece validateOnly en true para validar la solicitud sin aplicar los cambios. Cuando esté todo listo para aplicar los cambios, establece validateOnly en false.

Si instalaste una biblioteca cliente, selecciona la pestaña del lenguaje de programación que elegiste para ver un ejemplo de código completo sobre cómo crear y enviar una solicitud.

REST

{
    "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",
    "events": [
        {
            "adIdentifiers": {
                "gclid": "GCLID_1"
            },
            "conversionValue": 30.03,
            "currency": "USD",
            "eventTimestamp": "2025-06-10T20:07:01Z",
            "transactionId": "ABC798654321",
            "eventSource": "WEB",
            "userData": {
                "userIdentifiers": [
                    {
                        "address": {
                            "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A",
                            "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
                            "regionCode": "US",
                            "postalCode": "94045"
                        }
                    }
                ]
            },
            "userProperties": {
                "customerType": "NEW",
                "customerValueBucket": "HIGH"
            },
            "eventName": "purchase",
            "clientId": "1234567890.1761581763",
            "userId": "user_ABC12345",
            "additionalEventParameters": [
                {
                    "parameterName": "ad_unit_name",
                    "value": "Banner_01"
                }
            ],
            "cartData": {
                "transactionDiscount": 6.66,
                "items": [
                    {
                        "itemId": "SKU_12345",
                        "quantity": 3,
                        "unitPrice": 10.01,
                        "additionalItemParameters": [
                            {
                                "parameterName": "item_name",
                                "value": "Stan and Friends Tee"
                            },
                            {
                                "parameterName": "affiliation",
                                "value": "Google Merchandise Store"
                            },
                            {
                                "parameterName": "coupon",
                                "value": "SUMMER_FUN"
                            },
                            {
                                "parameterName": "discount",
                                "value": "2.22"
                            },
                            {
                                "parameterName": "index",
                                "value": "0"
                            },
                            {
                                "parameterName": "item_brand",
                                "value": "Google"
                            },
                            {
                                "parameterName": "item_category",
                                "value": "Apparel"
                            },
                            {
                                "parameterName": "item_category2",
                                "value": "Adult"
                            },
                            {
                                "parameterName": "item_category3",
                                "value": "Shirts"
                            },
                            {
                                "parameterName": "item_category4",
                                "value": "Crew"
                            },
                            {
                                "parameterName": "item_category5",
                                "value": "Short sleeve"
                            },
                            {
                                "parameterName": "item_list_id",
                                "value": "related_products"
                            },
                            {
                                "parameterName": "item_list_name",
                                "value": "Related Products"
                            }
                        ]
                    }
                ]
            }
        },
        {
            "adIdentifiers": {
                "gclid": "GCLID_2"
            },
            "conversionValue": 42.02,
            "currency": "EUR",
            "eventTimestamp": "2025-06-11T04:42:33Z",
            "transactionId": "DEF999911111",
            "eventSource": "WEB",
            "userData": {
                "userIdentifiers": [
                    {
                        "emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250"
                    },
                    {
                        "emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4"
                    },
                    {
                        "address": {
                            "givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450",
                            "familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F",
                            "regionCode": "PT",
                            "postalCode": "1229-076"
                        }
                    }
                ]
            },
            "userProperties": {
                "customerType": "RETURNING"
            },
            "eventName": "purchase",
            "clientId": "9876543210.1761582117",
            "userId": "user_DEF9876",
            "additionalEventParameters": [
                {
                    "parameterName": "ad_unit_name",
                    "value": "Banner_02"
                }
            ],
            "cartData": {
                "transactionDiscount": 6.66,
                "items": [
                    {
                        "itemId": "SKU_12346",
                        "quantity": 2,
                        "unitPrice": 21.01,
                        "additionalItemParameters": [
                            {
                                "parameterName": "item_name",
                                "value": "Google Grey Women's Tee"
                            },
                            {
                                "parameterName": "affiliation",
                                "value": "Google Merchandise Store"
                            },
                            {
                                "parameterName": "coupon",
                                "value": "SUMMER_FUN"
                            },
                            {
                                "parameterName": "discount",
                                "value": "3.33"
                            },
                            {
                                "parameterName": "index",
                                "value": "1"
                            },
                            {
                                "parameterName": "item_brand",
                                "value": "Google"
                            },
                            {
                                "parameterName": "item_category",
                                "value": "Apparel"
                            },
                            {
                                "parameterName": "item_category2",
                                "value": "Adult"
                            },
                            {
                                "parameterName": "item_category3",
                                "value": "Shirts"
                            },
                            {
                                "parameterName": "item_category4",
                                "value": "Crew"
                            },
                            {
                                "parameterName": "item_category5",
                                "value": "Short sleeve"
                            },
                            {
                                "parameterName": "item_list_id",
                                "value": "related_products"
                            },
                            {
                                "parameterName": "item_list_name",
                                "value": "Related Products"
                            }
                        ]
                    }
                ]
            }
        }
    ],
    "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}");
            }
            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

// 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);
    }
  }
}

Nodo

#!/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/data-manager-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);
  }
  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
// 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";
        }
        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

#!/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)

    _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,
    )

Respuestas de éxito

Una solicitud correcta devuelve una respuesta con un objeto que contiene un requestId.

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

Registra el requestId que se devolvió para que puedas recuperar los diagnósticos a medida que se procesa cada destino en la solicitud.

Respuestas de error

Una solicitud fallida genera un código de estado de respuesta de error, como 400 Bad Request, y una respuesta con detalles del error.

Por ejemplo, un emailAddress que contiene una cadena de texto sin formato en lugar de un valor codificado en hexadecimal produce la siguiente respuesta:

{
  "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"
          }
        ]
      }
    ]
  }
}

Un emailAddress que no se hashea y solo se codifica en hexadecimal produce la siguiente respuesta:

{
  "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"
          }
        ]
      }
    ]
  }
}

Envía eventos para varios destinos

Si tus datos contienen eventos para diferentes destinos, puedes enviarlos en la misma solicitud usando referencias de destino.

Por ejemplo, si tienes un evento para el ID de acción de conversión 123456789 y otro para el ID de acción de conversión 777111122, envía ambos eventos en una sola solicitud configurando el reference de cada Destination. El reference lo define el usuario. El único requisito es que cada Destination tenga un reference único. Esta es la lista de destinations modificada para la solicitud:

  "destinations": [
    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_TYPE",
        "accountId": "OPERATING_ACCOUNT_ID"
      },

      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_TYPE",
        "accountId": "LOGIN_ACCOUNT_ID"
      },

      "productDestinationId": "PRODUCT_DESTINATION_ID",
      "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"
    }
  ]

Establece el destinationReferences de cada Event para enviarlo a uno o más destinos específicos. Por ejemplo, aquí hay un Event que solo es para el primer Destination, por lo que su lista de destinationReferences solo contiene el reference del primer Destination:

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

El campo destinationReferences es una lista, por lo que puedes especificar varios destinos para un evento. Si no estableces el destinationReferences de un objeto Event, la API de Data Manager envía el evento a todos los destinos de la solicitud.

Si un evento tiene varios destinos, la API de Data Manager envía los campos pertinentes a cada uno de ellos. Por ejemplo, si un evento tiene un destino de Google Ads y un destino de Google Analytics, la API incluye campos de Google Analytics, como clientId o eventName, cuando envía el evento al destino de Google Analytics, y campos de Google Ads, como customVariables, cuando envía el evento al destino de Google Ads.

Próximos pasos