傳送事件

您可以完成本快速入門導覽課程,熟悉如何傳送事件資料。

在下列任一情境中,使用 Data Manager API:

  • 將 Google Ads 代碼轉換或 Google Analytics purchase 事件做為代碼轉換的額外資料來源,提升廣告互動信號、強化資料和整體成效。

    所有 Google Ads 帳戶都能使用這項功能,但只有在許可清單上的 Google Analytics 資源才能使用。如有意將 Google Analytics 資源加入允許清單,請填寫這份表單

  • 傳送 Google Ads 離線轉換待開發客戶強化轉換的事件資料。

選擇要查看的指南版本:

在本快速入門導覽課程中,您將完成下列步驟:

  1. 準備Destination接收事件資料。
  2. 準備要傳送的事件資料。
  3. 為事件建立 IngestionService 要求。
  4. 使用 Google APIs Explorer 傳送要求。
  5. 瞭解成功和失敗的回應。

準備目的地

傳送資料前,您必須先準備至少一個資料的 Destination。以下是可供使用的 Destination 範例:

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

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

      "productDestinationId": "PRODUCT_DESTINATION_ID"
    }

Destination 的欄位如下:如需更多詳細資料和不同情境的目標範例,請參閱「設定目的地」。

operatingAccount

接收事件的帳戶。

如果是以額外資料來源傳送的事件,作業帳戶可以是 Google Ads 帳戶或 Google Analytics 資源。

如果 accountTypeGOOGLE_ANALYTICS_PROPERTY,則要求憑證必須屬於 Google Analytics 使用者,且該使用者具備資源的編輯者或管理員角色

如果是離線轉換和待開發客戶強化轉換,則運作中的帳戶必須是 Google Ads 帳戶。

loginAccount
憑證的 Google 帳戶是使用者的帳戶。
productDestinationId

接收事件的 operatingAccount 中實體的 ID。

如果事件是做為額外資料來源傳送,productDestinationId 必須是下列其中一項:

  1. Google Ads 轉換的 ID,其中 type 設為 WEBPAGE。在 Google Ads 使用者介面中,WEBPAGE 轉換動作的「轉換來源」為「網站」

  2. Google Analytics 網站串流的評估 ID。您無法將事件做為額外資料來源,傳送至 Google Analytics iOS 應用程式或 Android 應用程式串流。

如果是離線轉換或待開發客戶強化轉換,productDestinationId 必須是 Google Ads 轉換動作的 ID,且 type 設為 UPLOAD_CLICKS。在 Google Ads 使用者介面中,UPLOAD_CLICKS 轉換動作的「轉換來源」為「網站 (從點擊匯入)」

本指南中的範例說明如何建構要求,將所有事件傳送至相同目的地。如要在同一個要求中傳送多個目的地的事件,請參閱「傳送多個目的地的事件」。

準備事件資料

請參考下列事件資料。每個表格都對應一個轉換事件。每個轉換事件都有事件的時間戳記、轉換動作和轉換價值。

每個事件可能都有廣告 ID (例如 gclid) 或使用者 ID (例如電子郵件地址、電話號碼和地址資訊)。事件也可以有:

  • 在事件發生時評估使用者相關資訊,例如顧客價值,或是新顧客、回訪顧客或重新參與的顧客。
  • 購物車資料。
  • 目的地適用的其他事件參數或使用者屬性,例如 Google Analytics 的 client_iduser_id

活動資料如下:

事件 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

設定資料格式

按照格式設定指南的規定設定欄位格式。以下是格式化後的事件資料:

事件 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

雜湊並編碼資料

此外,格式化的電子郵件地址、名字和姓氏必須使用 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

將資料轉換為 Event 物件

將每個事件的格式化和雜湊處理資料轉換為 Event。請按照指示填入下列欄位:

  1. eventTimestamp 設為事件發生的時間。

    Google Analytics 事件的 eventTimestamp 必須在過去 72 小時內。

  2. 根據用途設定必填欄位。

    用途 ID transactionId eventSource
    離線轉換或待開發客戶強化轉換 必填。設定至少下列其中一項: 選用 必填。設為 EventSource 的其中一個列舉值。
    傳送至 Google Ads 目的地做為額外資料來源的事件 必填。設定至少下列其中一項: 必要 (選用步驟) 如要設定,必須為 WEB
    傳送至 Google Analytics 目的地的事件,做為額外資料來源 必填。設定至少下列其中一項: 必要 (選用步驟) 如要設定,必須為 WEB
  3. 如果您將事件做為額外資料來源傳送至 Google Ads 目的地,請參閱「Google 如何處理額外資料來源的資料」。

  4. 填入您有事件值的任何其他欄位。如需可用欄位的完整清單,請參閱 Event 參考說明文件。

Google 如何處理額外資料來源的資料

在同一個轉換動作中,Google 會使用 transactionId,將不同來源 (例如網站代碼和 Data Manager API 擷取要求) 傳送的轉換事件進行去重複處理。下表說明系統如何處理擷取要求中的資料。

情境 資料欄位 處理方式
transactionId 與現有代碼事件相符 conversionValue (含 currencyCode)

已更新。conversionValue (含 currencyCode) 中的值會Event覆寫代碼記錄的原始值。

注意:轉換動作的初始 14 天試用期內,系統會停用價值更新功能。試用期結束前,系統不會在 Google Ads 報表中覆寫代碼價值。

transactionId 符合現有代碼事件 conversionValuecurrencyCode 以外的其他欄位 (例如 adIdentifiers.gclid) 已忽略。其他資料來源的其他欄位值不會覆寫 Google 代碼原本為比對相符交易記錄的欄位值。
transactionId 與任何現有事件都「不」相符 所有提供的資料 (例如 userDataconversionValuecurrencyCode)

用於建立新的轉換事件。Google 接著會嘗試使用您提供的 ID (例如 adIdentifiers.gcliduserData),將這項新轉換歸因於廣告點擊。

注意:在最初的 14 天試用期內,這些新建立的轉換會顯示在報表中,但不會用於出價。試用期結束後,這些轉換就會自動用於出價。

新增工作階段屬性

如果您要傳送離線轉換或待開發客戶強化轉換,請在沒有其他廣告 ID (例如 Google 點擊 ID 或 WBRAID) 時,加入工作階段屬性。除了其他廣告 ID 之外,您也可以納入工作階段屬性。

工作階段屬性可提供使用者與網站互動的額外背景資訊和信號,有助於提升轉換評估、報表和出價的準確度。

在 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

    以下是範例事件的部分內容,其中包含 experimentalFieldsgad_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 事件的名稱。

transactionId

必要。事件的專屬 ID。

至少一個 ID

必須設定下列至少其中一個欄位:

destinationReferences

如果要求層級的 destinations 清單包含多個 Google Analytics Destination,則為必填。在 destinationReferences 中新增項目,指定應接收事件的 Google Analytics 目的地。如要進一步瞭解目的地參照,請參閱「將事件傳送至多個目的地」。

如果未設定 destinationReferences,或有多個參照 Google Analytics 目的地的項目,Data Manager API 會拒絕事件,並傳回 MULTIPLE_DESTINATIONS_FOR_GOOGLE_ANALYTICS_EVENT 錯誤。

userId

(選用步驟) 使用者的 User-ID

additionalEventParameters

(選填,但建議使用) 填入其他 Event 欄位未擷取的任何 Google Analytics 事件參數。參數可包含purchase事件中的其他建議參數,或您想擷取的其他參數。使用 EventParameterparameterName Google Analytics 參數名稱。

舉例來說,如果交易有相關稅金,請在 additionalEventParameters 中新增項目,並將 parameterName 設為 tax,以及將 value 設為稅金費用。

我們不建議為 transactionIdcurrencyvalue Google Analytics 事件參數新增項目。請改為填入 EventtransactionIdcurrencyconversionValue,這些屬性的優先順序高於 additionalEventParameters 中的任何項目。

為購買事件新增購物車資料

EventcartData 欄位中,填入所購商品的相關資訊。為每項購買的商品,在 CartDataitems 清單中新增 Item 物件,並填入下列欄位:

itemId
必要。商品的專屬 ID。
unitPrice

必填。此為單位價格,不含稅金、運費和事件範圍 (交易層級) 折扣。

如果商品有商品範圍折扣,請使用折扣單價。 舉例來說,如果商品的單價為 27.67,單一折扣為 6.66,則請將 unitPrice 設為 21.01

quantity

必填。這項商品購買的單位數。

additionalItemParameters

請在其他 Item 欄位中未擷取的任何項目範圍參數,填入這份清單。使用 parameterName 的 Google Analytics 項目參數名稱,適用於 ItemParameter

舉例來說,如果項目有品牌和類別,請在項目的 additionalItemParameters 中新增項目,並將 parameterName 設為 item_brand,將 value 設為品牌名稱;另一個項目則將 parameterName 設為 item_category,將 value 設為項目的類別。

我們不建議為 quantitypriceitem_id Google Analytics 項目參數新增項目。請改為填入 ItemitemIdunitPricequantity,這些屬性的優先順序高於 additionalItemParameters 中的任何項目。

以下是第二個事件的格式化、雜湊處理和編碼資料範例,其中包含 Google Analytics 的額外資料:Event

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

建立要求主體

如要建構要求主體,請合併 destinationsevents,設定 encoding 欄位,並新增您想加入的任何其他要求欄位,例如 validateOnlyconsent

本指南中的範例不會使用加密功能,但您可以按照「加密使用者資料」一文中的說明,在程序中加入加密功能。

傳送要求

如要透過瀏覽器提出要求,請按照下列步驟操作:

  1. 選取「REST」分頁,然後按一下「Open in API Explorer」,在新分頁或視窗中開啟 API Explorer。
  2. 在 API 探索工具的要求內容中,將開頭為 REPLACE_WITH 的每個字串 (例如 REPLACE_WITH_OPERATING_ACCOUNT_TYPE) 替換為相關值。
  3. 按一下 API Explorer 頁面底部的「執行」,然後完成授權提示,即可傳送要求。
  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
}

.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": "PRODUCT_DESTINATION_ID",
      "reference": "destination_a"
    },
    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_2_TYPE",
        "accountId": "OPERATING_ACCOUNT_2_ID"
      },

      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_2_TYPE",
        "accountId": "LOGIN_ACCOUNT_2_ID"
      },

      "productDestinationId": "777111122",
      "reference": "destination_b"
    }
  ]

設定每個 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 目的地時,會加入 Google Analytics 欄位,例如 clientIdeventName;當 API 將事件傳送至 Google Ads 目的地時,則會加入 Google Ads 欄位,例如 customVariables

後續步驟