发送事件

请按照本指南操作,熟悉使用 Data Manager API 发送事件的流程。

在以下任一情况下使用 Data Manager API:

  • 将包含交易 ID 的 Google Ads 代码转化或任何 Google Analytics 事件作为代码或 Google Analytics for Firebase (GA4F) 事件的额外数据源发送,以最大限度地提高广告互动信号,并增强数据和整体效果。

    此功能适用于所有 Google Ads 账号,但仅适用于已列入许可名单的 Google Analytics 媒体资源。如果您有兴趣将自己的 Google Analytics 媒体资源添加到许可名单中,请填写此表单

  • 发送 Google Ads 线下转化增强型潜在客户转化的事件数据。

  • 发送 Google Ads 实体店销售转化数据。此功能仅适用于许可名单中的 Google Ads 账号。

  • 向 Google Analytics 发送推荐事件自定义事件

在本指南中,您将完成以下步骤:

  1. 准备一个 Destination 来接收事件数据。
  2. 准备要发送的事件数据。
  3. 为事件构建 IngestionService 请求。
  4. 使用 Google APIs Explorer 发送请求。
  5. 了解成功和失败响应。

准备目的地

在发送数据之前,您需要为数据准备至少一个 Destination。以下是 Destination 的字段。如需了解详情和查看不同场景下的目标平台示例,请参阅配置目标平台

选择与您的使用情形对应的标签页。

  • 如果您要管理的广告客户账号中包含某个 Google 账号,并且您要使用该账号的凭据,请选择广告客户
  • 如果您使用的是 Google 账号的凭据,而该账号是数据合作伙伴账号中的用户,并且您想管理与数据合作伙伴账号建立合作伙伴关联的广告客户账号,请选择数据合作伙伴

广告客户

operatingAccount

接收活动的账号。

对于作为附加数据源发送的事件,操作账号可以是 Google Ads 账号,也可以是 Google Analytics 媒体资源。

如果 accountTypeGOOGLE_ANALYTICS_PROPERTY,则请求的凭据必须适用于对相应媒体资源拥有“编辑者”或“管理员”角色的 Google Analytics 用户

对于线下转化、增强型潜在客户转化、实体店销售转化和含购物车数据的转化,操作账号必须是 Google Ads 账号。

loginAccount

凭据对应的 Google 账号是用户的账号。

productDestinationId

operatingAccount 中接收事件的实体的 ID。

对于作为额外数据源发送的事件,productDestinationId 必须是以下值之一:

  1. type 设置为 WEBPAGE 的 Google Ads 转化 ID。在 Google Ads 界面中,WEBPAGE 转化操作的转化来源网站

  2. Google Analytics 网站数据流的衡量 ID

  3. Google Analytics iOS 或 Android 应用数据流的 Firebase 应用 ID

对于发送到 Google Analytics 的其他事件,productDestinationId 必须是以下值之一:

  1. Google Analytics 网站数据流的衡量 ID

  2. Google Analytics iOS 或 Android 应用数据流的 Firebase 应用 ID

对于线下转化或增强型潜在客户转化,productDestinationId 必须是 type 设置为 UPLOAD_CLICKS 的 Google Ads 转化操作的 ID。在 Google Ads 界面中,UPLOAD_CLICKS 转化操作的转化来源网站(从点击导入)

对于实体店销售转化,productDestinationId 必须是 type 设置为 STORE_SALES 的 Google Ads 转化操作的 ID。在 Google Ads 界面中,STORE_SALES 转化操作的转化来源实体店销售

{
  "operatingAccount": {
    "accountType": "OPERATING_ACCOUNT_TYPE",
    "accountId": "OPERATING_ACCOUNT_ID"
  },
  "loginAccount": {
    "accountType": "LOGIN_ACCOUNT_TYPE",
    "accountId": "LOGIN_ACCOUNT_ID"
  },
  "productDestinationId": "PRODUCT_DESTINATION_ID"
}

数据合作伙伴

operatingAccount
接收事件的账号。必须是 Google Ads 账号。Data Manager API 不支持使用关联的数据合作伙伴将事件发送到 Google Analytics 媒体资源。
loginAccount

凭据的用户有权访问的账号。

accountId 设置为数据合作伙伴账号 ID,并将 accountType 设置为 DATA_PARTNER

linkedAccount

已建立产品关联的账号,凭据的用户可通过该账号访问 operatingAccount

如果 operatingAccount父级与您的数据合作伙伴账号相关联,请将 linkedAccount 设置为 operatingAccount 的父级。如果 operatingAccount 直接关联到您的数据合作伙伴账号,请勿设置 linkedAccount

productDestinationId

operatingAccount 中接收事件的实体的 ID。

对于使用关联的数据合作伙伴作为额外数据源发送的事件,productDestinationId 必须是 type 设置为 WEBPAGE 的 Google Ads 转化 ID。在 Google Ads 界面中,WEBPAGE 转化操作的转化来源网站

对于线下转化或增强型潜在客户转化,productDestinationId 必须是 type 设置为 UPLOAD_CLICKS 的 Google Ads 转化操作的 ID。在 Google Ads 界面中,UPLOAD_CLICKS 转化操作的转化来源网站(从点击导入)

对于实体店销售转化,productDestinationId 必须是 type 设置为 STORE_SALES 的 Google Ads 转化操作的 ID。在 Google Ads 界面中,STORE_SALES 转化操作的转化来源实体店销售

{
  "operatingAccount": {
    "accountType": "OPERATING_ACCOUNT_TYPE",
    "accountId": "OPERATING_ACCOUNT_ID"
  },
  "loginAccount": {
    "accountType": "DATA_PARTNER",
    "accountId": "DATA_PARTNER_ACCOUNT_ID"
  },
  "linkedAccount": {
    "accountType": "LINKED_ACCOUNT_TYPE",
    "accountId": "LINKED_ACCOUNT_ID"
  },
  "productDestinationId": "PRODUCT_DESTINATION_ID"
}

本指南中的示例展示了如何构建将所有事件发送到同一目的地的请求。如果您想在同一请求中为多个目的地发送事件,请参阅为多个目的地发送事件

准备事件数据

请考虑以下事件数据。每个表都对应一个转化事件。每个转化事件都包含事件的时间戳、转化操作和转化价值。

每个事件可能都有广告标识符(例如 gclid)或用户标识符(例如电子邮件地址、电话号码和地址信息)。事件还可以包含:

  • 在发生事件时评估的用户相关信息,例如客户价值,或者客户是新客户、回访客户还是再次吸引的客户。
  • 购物车数据
  • 目的地的其他事件参数或用户属性,例如 Google Analytics 的 clientIdappInstanceIduserId

以下是事件数据:

事件 1

活动 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
购物车中的商品
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

事件 2

活动 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
购物车中的商品
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

事件 3

活动 3(实体店销售额)
conversion_time June 11, 2025 12:05:00PM America/New_York
conversion_action_id 123456789
transaction_id XYZ987654321
conversion_value 450.0
currency usd
given_name Charlie
family_name Smith-Jones
region_code US
postal_code 10011
store_id STORE_NY_001
merchant_id 123
merchant_feed_label US
merchant_feed_language_code en
购物车中的商品
merchant_product_id ITEM_SKU_987
item_quantity 1
item_price 450.0
variable DEPARTMENT
value ELECTRONICS

设置数据格式

根据格式设置指南中的规定设置字段格式。以下是格式化后的活动数据:

事件 1

活动 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
购物车中的商品
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

事件 2

活动 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
购物车中的商品
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

事件 3

活动 3(实体店销售额)
conversion_time 2025-06-11T12:05:00-04:00
conversion_action_id 123456789
transaction_id XYZ987654321
conversion_value 450.0
currency USD
given_name charlie
family_name smith-jones
region_code US
postal_code 10011
store_id STORE_NY_001
merchant_id 123
merchant_feed_label US
merchant_feed_language_code en
购物车中的商品
merchant_product_id ITEM_SKU_987
item_quantity 1
item_price 450.0
variable DEPARTMENT
value ELECTRONICS

对数据进行哈希处理和编码

此外,格式化后的电子邮件地址、名字和姓氏必须使用 SHA-256 算法进行哈希处理,并使用十六进制或 Base64 编码进行编码。以下是使用十六进制编码进行格式设置、哈希处理和编码后的事件数据:

事件 1

活动 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
购物车中的商品
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

事件 2

活动 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
购物车中的商品
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

事件 3

活动 3(实体店销售额)
conversion_time 2025-06-11T12:05:00-04:00
conversion_action_id 123456789
transaction_id XYZ987654321
conversion_value 450.0
currency USD
given_name B9DD960C1753459A78115D3CB845A57D924B6877E805B08BD01086CCDF34433C
family_name DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081
region_code US
postal_code 10011
store_id STORE_NY_001
merchant_id 123
merchant_feed_label US
merchant_feed_language_code en
购物车中的商品
merchant_product_id ITEM_SKU_987
item_quantity 1
item_price 450.0
variable DEPARTMENT
value ELECTRONICS

将数据转换为 Event 对象

将每个事件的格式化和哈希处理后的数据转换为 Event。按如下所示填充以下字段:

  1. eventTimestamp 设置为事件发生的时间。

    Google Analytics 的事件必须具有过去 72 小时内的 eventTimestamp

  2. 为您的 Google Ads 或 Google Analytics 使用情形设置必需字段。

    使用场景 标识符 transactionId eventSource
    线下转化或增强型潜在客户转化 必需。设置以下至少一项: 可选 必需。设置为 EventSource 的某个枚举值。
    作为附加数据源发送的事件 必需。设置以下至少一项: 必需 可选。如果设置,则必须为 WEB
    实体店销售转化
    • 必需。设置 Event 的以下字段:
      • eventLocation.storeId
      • currency
      • conversionValue
    • 必需。设置以下字段以提供用户标识符:
      • 如果用户标识符来自第三方来源,则填充 thirdPartyUserData。 仅当 Destination 的登录账号是数据合作伙伴(loginAccount.accountTypeDATA_PARTNER)时,才允许设置 thirdPartyUserData
      • 否则,填充 userData
    必需 必需。设置为 IN_STORE

    Google Analytics

    使用场景 标识符 transactionId eventSource
    作为附加数据源发送到网站数据流的事件 必需。设置以下至少一项: 必需 可选。如果设置,则必须为 WEB
    作为附加数据源发送到应用数据流的事件

    必需。设置为 appInstanceId

    使用 gbraidgclid 设置可选字段(例如 userIdadIdentifiers),以进一步增强衡量效果。

    必需 可选。如果设置,则必须为 APP
    发送到网站数据流的其他事件 必需。设置 clientId 对于某些活动(例如 purchaserefund),此属性为必需属性。如需了解详情,请查看特定活动的具体要求 可选。如果设置,则必须为 WEB
    发送到应用数据流的其他事件 必需。设置 appInstanceId 对于某些活动(例如 purchaserefund),此属性为必需属性。如需了解详情,请查看特定活动的具体要求 可选。如果设置,则必须为 APP
  3. 如果您要将事件作为附加数据源发送,请查看 Google 如何处理附加数据源中的数据

  4. 如果您要向 Google Analytics 发送事件,请向事件添加 Google Analytics 信息

  5. 如果您要发送含购物车数据的 Google Ads 转化,或者要将购物车信息纳入 Google Ads 实体店销售转化中,则需要向事件添加购物车数据。这些事件包括需要 items 列表的 Google Analytics 推荐事件(例如 purchase)。

  6. 填写您知道相应活动值的任何其他字段。如需查看可用字段的完整列表,请参阅 Event 参考文档。

Google 如何处理附加数据源中的数据

在同一转化操作中,Google 会使用 transactionId 来去重来自不同来源(例如网站代码和 Data Manager API 提取请求)的转化事件。下表说明了如何处理提取请求中的数据。

场景 数据字段 处理方式
transactionId 匹配现有代码事件 conversionValue(带 currencyCode

已更新。来自 EventconversionValue(含 currencyCode)会替换代码记录的原始值。

注意:在转化操作的初始 14 天试用期内,价值更新功能处于停用状态。在试用期结束之前,Google Ads 报告中不会替换该代码的值。

transactionId 匹配现有代码事件 conversionValuecurrencyCode 以外的其他字段(例如,adIdentifiers.gclid 忽略。来自额外数据源的其他字段值不会覆盖 Google 代码最初为匹配的交易记录的字段值。
transactionId 与任何现有事件都不匹配 所有提供的数据(例如 userDataconversionValuecurrencyCode

用于创建新的转化事件。然后,Google 会尝试使用您提供的标识符(例如 adIdentifiers.gcliduserData)将此新转化归因于广告点击。

注意:在最初的 14 天试用期内,这些新创建的转化会显示在报告中,但不会用于出价。试用期结束后,这些转化会自动变为可出价的转化。

添加会话属性

如果您要发送线下转化数据或增强型潜在客户转化数据,请在没有其他广告标识符(例如 GCLID 或 WBRAID)的情况下添加会话属性。除了其他广告标识符之外,您还可以添加会话属性。

会话属性可提供有关用户与您网站的互动的更多背景信息和信号,从而提升转化衡量、报告和出价的准确性。

在 Data Manager API 中,您可以使用两种方法来发送会话属性:

  1. 建议:将 adIdentifierssessionAttributes 字段设置为采用 base64 编码的会话属性字符串。按照如何捕获 session_attributes 中的说明修改表单提交页面,以捕获编码后的字符串。

  2. 如果您无法使用 JavaScript,请捕获各个会话属性字段,并将每个字段作为单独的 ExperimentalField 添加到 experimentalFields 列表中:

    • gad_campaignid
    • session_start_time_usec
    • gad_source
    • landing_page_url
    • landing_page_referrer

    如果您有 landing_page_user_agent 会话属性的值,请在 adIdentifiers.landingPageDeviceInfouserAgent 字段中发送该值。

    以下是发送单个键值对时的最佳实践:

    • 持续发送 gad_campaignidsession_start_time_usec。这些字段对于准确归因至关重要。
    • 请勿提供不准确或不完整的 landing_page_url 值,例如占位字符串、内部应用路径或不完整的网址。如果您没有准确的完整网址,请省略 landing_page_url

    以下是一个示例事件的部分内容,其中 experimentalFields 包含 gad_campaignidsession_start_time_usec 的条目,并且 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"
        }
      }
    }
    
    

添加 Google Analytics 信息

如果作为附加数据源发送的事件的目标位置包含 Google Analytics 媒体资源,请按指示填充以下字段:

eventName

必需。Google Analytics 事件的名称。

如果事件名称是预留名称,Data Manager API 会拒绝该事件,并返回错误 INVALID_EVENT_NAME

transactionId

如果Google Analytics 事件规范中指明该参数是必需的,则为必需

标识符

以下是每种类型的数据流的标识符要求:

网站数据流

如果发送的事件包含交易 ID 作为附加数据源,则必须设置以下字段中的至少一个:

对于所有其他推荐事件或自定义事件,clientId 是必需的。请参阅 Google Analytics Measurement Protocol 说明

应用数据流

destinationReferences

如果请求级 destinations 列表包含多个 Google Analytics Destination,则此属性为必需属性。向 destinationReferences 添加一个条目,以指定哪个 Google Analytics 目标账号应接收相应事件。如需详细了解目的地引用,请参阅将事件发送到多个目的地

如果未设置 destinationReferences 或该参数包含多个指向 Google Analytics 目的地的条目,Data Manager API 会拒绝该事件,并返回 MULTIPLE_DESTINATIONS_FOR_GOOGLE_ANALYTICS_EVENT 错误。

cartData

如果Google Analytics 事件规范中指明该参数是必需的,则为必需

了解如何向事件添加购物车数据

userId

可选。用户的 User-ID

userProperties

可选。用户的用户属性。为每个用户属性向 additionalUserProperties 列表添加单独的条目。

additionalEventParameters

可选,但建议填写。使用其他 Event 字段中未捕获的任何 Google Analytics 事件参数填充此列表。这些参数可以包括建议为相应事件添加的参数,也可以包括您想捕获的任何其他参数。针对 EventParameterparameterName 使用 Google Analytics 参数名称。

例如,如果您有与交易相关的税费,请向 additionalEventParameters 添加一个条目,并将 parameterName 设置为 tax,并将 value 设置为税费。

我们不建议向 additionalEventParameters 添加 transactionIdcurrencyvalue Google Analytics 事件参数的条目。请改为填充 EventtransactionIdcurrencyconversionValue 字段,这些字段的优先级高于 additionalEventParameters 中的任何条目。

添加购物车数据

使用与活动关联的商品的相关信息填充 EventcartData 字段。发送需要 items 列表的 Google Analytics 推荐事件(例如 purchase)或包含购物车数据参数的 Google Ads 转化时,请使用此字段。

根据需要填充 CartData 的以下字段。

items
必需。此列表中必须包含至少一个项目

Merchant Center 字段

merchantIdmerchantFeedLabelmerchantFeedLanguageCode 都是可选的。如果事件中的商品包含多个 Merchant Center 账号中都存在的产品,请为含购物车数据的 Google Ads 转化设置这些字段。

merchantId
可选。Merchant Center 账号 ID。
merchantFeedLabel
可选。Merchant Center Feed 的 Feed 标签。借助 Feed 标签,您可以对商品进行分类,以便进行广告系列定位。例如,您可以使用 Feed 标签按语言整理商品。如果您的广告系列根据国家/地区定位商品,请使用 ISO-3166-1 alpha-2 格式的 2 字母国家/地区代码。示例:US
merchantFeedLanguageCode
可选。与您上传商品的 Merchant Center Feed 相关联的 ISO 639-1 语言代码。示例:en

添加商品级购物车数据

CartDataitems 列表中添加一个或多个 Item 对象。对于包含购物车数据的 Google Ads 转化和需要 items 列表的 Google Analytics 推荐事件(例如 purchase),items 列表不得为空。

根据需要为每个 Item 填充以下字段。

Google Analytics

以下是 Google Analytics 推荐事件(例如需要 items 列表的 purchase)的要求。

items.itemId
必需。商品的唯一标识符。
items.unitPrice

可选。相应商品的单价(不含税、运费和事件级范围的[交易级] 折扣)。

如果商品有商品级折扣,请使用折扣后的单价。例如,如果某商品的单价为 27.67,单位折扣为 6.66,则将 unitPrice 设置为 21.01

items.quantity

必需。相应商品的单位数量。

items.additionalItemParameters

可选。使用其他 Item 字段中未捕获的任何商品级范围的参数填充此列表。使用 Google Analytics 文档中的参数名称作为 ItemParameterparameterName

例如,对于 Google Analytics,如果您有purchase 事件的商品的品牌和类别,请向商品的 additionalItemParameters 添加一个条目,并将 parameterName 设置为 item_brand,将 value 设置为品牌名称。

我们不建议为 quantitypriceitem_id 推荐项参数向 additionalItemParameters 添加条目。请改为填充 ItemitemIdunitPricequantity 字段,这些字段优先于 additionalItemParameters 中的任何条目。

以下是 Google Ads 实体店销售转化的相关要求。

items.itemId
可选。商品的唯一标识符。
items.merchantProductId
可选。Merchant Center 账号中的商品 ID
items.quantity
可选。购买的相应商品的数量。如果未设置,则默认为 1
items.unitPrice

可选。设置为相应商品的单价(不含税、运费和事件级 [交易级] 折扣)。

如果商品有商品级折扣,请使用折扣后的单价。例如,如果某商品的单价为 27.67,单位折扣为 6.66,则将 unitPrice 设置为 21.01

items.conversionValue

可选。相应商品的单个单位的转化价值。例如,如果购买了两件商品,每件商品的价格为 10.00 美元,请将 items.conversionValue 设置为 10.00,并将 items.quantity 设置为 2。请注意,虽然此字段对于单个商品是可选的,但在上传实体店销售交易转化数据时,必须提供事件级 conversionValue 字段。

items.customVariables

可选。要发送到 Google Ads 转化操作的其他键值对数据,以 CustomVariable 对象列表的形式指定。

以下是针对通过 Google Ads 转化(而非实体店销售转化)发送购物车数据的要求。如果您的事件是 Google Ads 实体店销售转化,请改为参阅 Google Ads 实体店销售要求。

items.itemId
必需。商品的唯一标识符。
items.merchantProductId
必需。Merchant Center 账号中的商品 ID
items.unitPrice

必需。 相应商品的单价(不含税、运费和事件级范围的[交易级] 折扣)。

如果商品有商品级折扣,请使用折扣后的单价。例如,如果某商品的单价为 27.67,单位折扣为 6.66,则将 unitPrice 设置为 21.01

items.quantity

必需。购买的相应商品的数量。

采样请求

Google Analytics

以下是第二个事件的格式化、哈希处理和编码数据的示例 Event,其中包含 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"
          }
        ]
      }
    ]
  }
}

以下是第三个事件的格式化、哈希处理和编码数据的示例 Event,该事件是 Google Ads 实体店销售转化:

{
  "conversionValue": 450.0,
  "currency": "USD",
  "eventTimestamp": "2025-06-11T12:05:00-04:00",
  "transactionId": "XYZ987654321",
  "eventSource": "IN_STORE",
  "eventLocation": {
    "storeId": "STORE_NY_001"
  },
  "userData": {
    "userIdentifiers": [
      {
        "address": {
          "givenName": "B9DD960C1753459A78115D3CB845A57D924B6877E805B08BD01086CCDF34433C",
          "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
          "regionCode": "US",
          "postalCode": "10011"
        }
      }
    ]
  },
  "cartData": {
    "merchantId": "123",
    "merchantFeedLabel": "US",
    "merchantFeedLanguageCode": "en",
    "items": [
      {
        "merchantProductId": "ITEM_SKU_987",
        "quantity": 1,
        "conversionValue": 450.0,
        "customVariables": [
          {
            "variable": "DEPARTMENT",
            "value": "ELECTRONICS"
          }
        ]
      }
    ]
  }
}

构建请求正文

如需构建请求正文,请合并 destinationsevents,设置 encoding 字段,并添加您要包含的任何其他请求字段,例如 validateOnlyconsent

本指南中的示例未使用加密,但您可以按照加密用户数据中的说明向流程添加加密。

发送请求

以下是在浏览器中尝试提出请求的步骤:

  1. 选择 REST 标签页,然后点击在 API Explorer 中打开,以在新标签页或窗口中打开 API Explorer。
  2. 在 API 探索器的请求正文中,将以 REPLACE_WITH 开头的每个字符串(例如 REPLACE_WITH_OPERATING_ACCOUNT_TYPE)替换为相关值。
  3. 点击 API 浏览器页面底部的执行,然后完成授权提示以发送请求。
  4. validateOnly 设置为 true,以验证请求但不应用更改。准备好应用更改后,请将 validateOnly 设置为 false

如果您安装了客户端库,请选择所选编程语言对应的标签页,查看有关如何构建和发送请求的完整代码示例。

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
}

数据合作伙伴

{
    "destinations": [
        {
            "operatingAccount": {
                "accountType": "OPERATING_ACCOUNT_TYPE",
                "accountId": "OPERATING_ACCOUNT_ID"
            },
            "loginAccount": {
                "accountType": "DATA_PARTNER",
                "accountId": "DATA_PARTNER_ACCOUNT_ID"
            },
            "linkedAccount": {
                "accountType": "LINKED_ACCOUNT_TYPE",
                "accountId": "LINKED_ACCOUNT_ID"
            },
            "productDestinationId": "CONVERSION_ACTION_ID"
        }
    ],
    "encoding": "HEX",
    "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);
    }
  }
}

节点

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

成功响应

如果请求成功,则会返回一个包含 requestId 的对象的响应。

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

记录返回的 requestId,以便在处理请求中的每个目的地时检索诊断信息

失败响应

如果请求失败,系统会返回错误响应状态代码(例如 400 Bad Request)以及包含错误详细信息的响应。

例如,包含纯文本字符串而非十六进制编码值的 emailAddress 会生成以下响应:

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

未经过哈希处理且仅经过十六进制编码的 emailAddress 会产生以下响应:

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

为多个目标平台发送事件

如果您的数据包含针对不同目标位置的事件,您可以使用目标位置引用在同一请求中发送这些事件。如需了解每个请求的目标数量上限,请参阅限制和配额

例如,如果您有一个转化操作 ID 为 123456789 的事件,以及另一个转化操作 ID 为 777111122 的事件,请通过设置每个 Destinationreference,在单个请求中发送这两个事件。reference 由用户定义。唯一的要求是每个 Destination 都具有唯一的 reference。以下是针对相应请求修改后的 destinations 列表:

广告客户

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

数据合作伙伴

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

设置每个 EventdestinationReferences,以将其发送到一个或多个特定目的地。例如,以下 Event 仅适用于第一个 Destination,因此其 destinationReferences 列表仅包含第一个 Destinationreference

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

destinationReferences 字段是一个列表,因此您可以为事件指定多个目的地。如果您未设置 EventdestinationReferences,Data Manager API 会将相应事件发送到请求中的所有目标。

如果某个事件有多个目标,Data Manager API 会将相关字段发送到每个目标。例如,如果某个事件同时具有 Google Ads 目标和 Google Analytics 目标,则当 API 将该事件发送到 Google Analytics 目标时,会包含 clientIdappInstanceIdeventName 等 Google Analytics 字段;而当 API 将该事件发送到 Google Ads 目标时,会包含 customVariables 等 Google Ads 字段。

后续步骤