傳送事件

請詳閱本指南,熟悉如何使用 Data Manager API 傳送 Google Ads 多來源轉換。

在本指南中,您將完成下列步驟:

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

準備目的地

傳送資料前,您必須為資料準備至少一個 Destination。以下是 Destination 的欄位。如需不同情境的目的地詳細資料和範例,請參閱「設定目的地」。

選取與用途對應的分頁。

  • 如果您使用的 Google 帳戶憑證是您要管理的廣告主帳戶使用者,請選取「廣告主」。
  • 如果您使用的 Google 帳戶憑證是資料合作夥伴帳戶中的使用者,且您想管理與資料合作夥伴帳戶有合作夥伴連結的廣告主帳戶,請選取「資料合作夥伴」。資料合作夥伴帳戶必須通過核准程序,才能核發。如要開始使用,請填寫意願調查表。

廣告主

operatingAccount

接收事件的帳戶。必須是擁有轉換動作的 Google Ads 帳戶。

將 accountType 設為 GOOGLE_ADS,並將 accountId 設為 10 位數 Google Ads 客戶 ID (不含連字號)。

loginAccount

憑證 Google 帳戶所屬的使用者帳戶。

productDestinationId

接收事件的 operatingAccount 中 Google Ads 轉換動作 ID。

轉換動作必須將 type 設為 WEBPAGE。在 Google Ads 使用者介面中,轉換動作的「轉換來源」為「網站」。WEBPAGE

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

資料合作夥伴

operatingAccount

接收事件的帳戶。必須是擁有轉換動作的 Google Ads 帳戶。

將 accountType 設為 GOOGLE_ADS,並將 accountId 設為 10 位數的 Google Ads 客戶 ID (不含連字號)。

loginAccount

憑證使用者有權存取的帳戶。

將 accountId 設為資料合作夥伴帳戶 ID,並將 accountType 設為 DATA_PARTNER。合作夥伴申請獲准後,Google 會提供資料合作夥伴帳戶 ID。

linkedAccount

已建立合作夥伴連結的帳戶,憑證使用者可透過該連結存取operatingAccount。

如果operatingAccount的父項已連結至資料合作夥伴帳戶,請將 linkedAccount 設為 operatingAccount 的父項。如果 operatingAccount 直接連結至資料合作夥伴帳戶,請勿設定 linkedAccount。

productDestinationId

接收事件的 operatingAccount 中 Google Ads 轉換動作 ID。

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

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

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

準備事件資料

在要求中傳送事件資料前,您必須先準備資料:

  1. 請按照格式設定指南的規定,設定原始值的格式。
  2. 電子郵件地址、名字和姓氏等使用者 ID 必須使用 SHA-256 演算法進行雜湊處理,並使用十六進位 (hex) 或 Base64 編碼。
  3. 使用格式化和雜湊值建構 Event 酬載。

下表顯示範例事件資料,從原始輸入資料經過格式化,到最終酬載值:

事件屬性 原始值 已設定格式 酬載值
event_timestamp 2025-06-10 15:07:01-05:00 2025-06-10T15:07:01-05:00 (ISO 8601) 2025-06-10T15:07:01-05:00
product_destination_id 123456789 123456789 123456789
transaction_id ABC798654321 ABC798654321 ABC798654321
conversion_value 30.03 30.03 30.03
currency USD USD USD
gclid GCLID_1 GCLID_1 GCLID_1
email_address john.smith@EXAMPLE.COM john.smith@example.com (已修剪,小寫) 8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0 (SHA-256 十六進位)
phone_number +1 (800) 555-0199 +18005550199 (E.164) 78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90 (SHA-256 十六進位)
given_name John john (已修剪,小寫) 96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A (SHA-256 十六進位)
family_name Smith-Jones smith-jones (已修剪,小寫) DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081 (SHA-256 十六進位)
region_code us US (雙字母 ISO 代碼) US
postal_code 94045 94045 94045
customer_type NEW NEW NEW
customer_value_bucket HIGH HIGH HIGH
購物車資料
item_id SKU_12345 SKU_12345 SKU_12345
merchant_product_id 12345 12345 12345
unit_price 10.01 10.01 10.01
quantity 3 3 3

將資料轉換為 Event 物件

將每個事件的格式化和雜湊處理資料轉換為 Event。

事件欄位規定

請根據下表中的規定建構事件。 如需可用欄位的完整清單,請參閱 Event 參考文件。

欄位 狀態 說明
eventTimestamp 必要 事件發生的時間。請參閱「時間戳記格式」。
transactionId 必要 轉換事件的專屬 ID,用於簡化 Google 代碼轉換。
eventSource 選用 事件的來源。如要設定,必須設為 WEB。
conversionValue 選用 與轉換相關聯的金額價值。
currency 選用 與此事件中的貨幣值相關聯的 3 碼 ISO 4217 貨幣代碼 (例如 USD 或 EUR)。
cartData 選用 相關聯的購物車和商品層級購買資料。如果提供,items 中至少一個項目為必填。請參閱「新增購物車資料」。
destinationReferences 必要 (有條件) 只有在單一要求中將事件傳送至多個目的地時,才需要此參數。
userProperties 選用 事件發生時評估的使用者相關資訊,例如 customerType (NEW 或 RETURNING) 或 customerValueBucket。
userId 選用 使用者的專屬 ID,
consent 選用 使用者的數位市場法 (DMA) 同意聲明設定,指定是否授予 adUserData 和 adPersonalization 的同意聲明。

識別 ID 規定

必填項目。您必須設定下列至少一項:

Google 如何處理多來源資料

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

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

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

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

transactionId 與現有代碼事件「相符」 userData 中使用者提供的資料,例如電子郵件地址、電話號碼或街道地址。
  • 補足缺漏資料:如果 Google 代碼擷取到轉換,但未能擷取任何使用者提供的資料,系統會成功插入您離線上傳提供的資料。

  • 如果代碼資料存在,系統會忽略:如果 Google 代碼成功擷取網站上的使用者提供資料,系統會忽略後續或不同的資料,不會覆寫或附加至現有代碼資料。

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

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

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

新增工作階段屬性

如果無法使用 GCLID 或 WBRAID 等其他廣告 ID,請新增工作階段屬性。除了其他廣告 ID 外,您也可以加入工作階段屬性。

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

選擇傳送工作階段屬性的方法:

  • 建議做法:將 adIdentifiers 的 sessionAttributes 欄位設為表單提交時擷取的 Base64 編碼工作階段屬性字串。

  • 替代方案:擷取個別工作階段屬性欄位,並將每個屬性新增至 experimentalFields 和 adIdentifiers 欄位中的事件。只有在無法擷取編碼的工作階段屬性字串時,才使用這個選項。

將 adIdentifiers 的 sessionAttributes 欄位設為 Base64 編碼的工作階段屬性字串。請按照「如何擷取 session_attributes」一文中的操作說明,修改表單提交頁面,擷取編碼字串。

以下是範例事件的一部分,其中包含 sessionAttributes 欄位中編碼的工作階段屬性:

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

另類

如果無法擷取編碼的工作階段屬性字串,請在 experimentalFields 清單中,為每個工作階段屬性新增 ExperimentalField。此外,請將使用者代理程式新增至 adIdentifiers 欄位的 landingPageDeviceInfo 欄位。

experimentalFields:

必要:為每個必要鍵,在 experimentalFields 中新增鍵/值組合項目:

  • gad_source:
  • gad_campaignid
  • session_start_time_usec

此外,您也可以為每個選用鍵新增鍵/值組合:

  • landing_page_url

  • landing_page_referrer

adIdentifiers:

必要:在 adIdentifiers.landingPageDeviceInfo 的 userAgent 欄位中加入到達網頁使用者代理程式。

以下是事件範例的一部分,其中 experimentalFields 包含 gad_source、gad_campaignid 和 session_start_time_usec 的項目,而 landingPageDeviceInfo 欄位則包含使用者代理程式:

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

新增購物車資料

在 Event 的 cartData 欄位中,填入與事件相關聯的商品資訊。傳送 Google Ads 轉換和購物車資料參數時,請使用這個欄位。

以下是 CartData 物件的欄位:

items
必要。這份清單中至少要有一個項目。

Merchant Center 欄位

merchantId、merchantFeedLabel 和 merchantFeedLanguageCode 欄位為選填。如果事件中的項目包含多個 Merchant Center 帳戶中的產品,請設定這些欄位。

merchantId
選用。Merchant Center 帳戶 ID。
merchantFeedLabel
選用。Merchant Center 動態饋給的動態饋給標籤。動態饋給標籤可協助你分類產品,以利設定廣告活動指定目標。舉例來說,你可以使用動態饋給標籤,依語言整理產品。如果廣告活動會根據國家/地區指定產品,請使用 ISO 3166-1 alpha-2 格式的雙字母國家/地區代碼 (例如 US)。
merchantFeedLanguageCode
選用。包含商品的 Merchant Center 動態饋給 ISO 639-1 語言代碼 (例如 en)。

項目欄位

將一或多個 Item 物件新增至 CartData 的 items 清單。提供 cartData 時,items 清單不得為空白。

請為每個 Item 填寫下列欄位:

items.itemId
必要。項目的專屬 ID。
items.merchantProductId
必要。Merchant Center 帳戶中的產品 ID。
items.unitPrice

必填。這項商品的單價,不含稅金、運費和以事件為範圍 (交易層級) 折扣。

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

items.quantity

必填。這項特定商品的購買數量。

要求範例

以下是事件中格式化、雜湊和編碼資料的 Event 範例,做為 Google Ads 多來源轉換與使用者資料的範例:

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

建構要求主體

如要建立要求主體,請合併 destinations 和 events、設定 encoding 欄位,並加入您要納入的任何其他要求欄位,例如 validateOnly 和 consent。

傳送要求

請按照下列步驟,嘗試透過瀏覽器提出要求:

  1. 選取「REST」分頁標籤,然後按一下「Open in API Explorer」(在 API Explorer 中開啟),在新分頁或視窗中開啟 API Explorer。
  2. 在 API Explorer 的要求主體中,將每個以 REPLACE_WITH 開頭的字串 (例如 REPLACE_WITH_OPERATING_ACCOUNT_TYPE) 替換成相關值。
  3. 按一下 APIs Explorer 頁面底部的「Execute」,並完成授權提示,即可傳送要求。
  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",
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "events": [
        {
            "adIdentifiers": {
                "gclid": "GCLID_1"
            },
            "conversionValue": 30.03,
            "currency": "USD",
            "eventTimestamp": "2025-06-10T15:07:01-05:00",
            "transactionId": "ABC798654321",
            "eventSource": "WEB",
            "userData": {
                "userIdentifiers": [
                    {
                        "emailAddress": "8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0"
                    },
                    {
                        "phoneNumber": "78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90"
                    },
                    {
                        "address": {
                            "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A",
                            "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081",
                            "regionCode": "US",
                            "postalCode": "94045"
                        }
                    }
                ]
            },
            "userProperties": {
                "customerType": "NEW",
                "customerValueBucket": "HIGH"
            }
        }
    ],
    "validateOnly": true
}

資料合作夥伴

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

.NET

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                var userDataBuilder = new UserData();

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

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

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

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

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

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

            IngestionServiceClient ingestionServiceClient = IngestionServiceClient.Create();

            int requestCount = 0;

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

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

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

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

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

Java

// 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 #%d:%n%s", requestCount, response));

        if (response.getFieldWarningsCount() > 0) {
          LOGGER.warning(
              "Request ingested successfully, but field warnings were returned. Review warning"
                  + " details and update your implementation as needed.");
        }
      }

      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/datamanager-util';
import * as fs from 'fs';
import * as yargs from 'yargs';

const MAX_EVENTS_PER_REQUEST = 10000;

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

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

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

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

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

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

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

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

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

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

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

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

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

    events.push(event);
  }

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

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

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

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

  const client = new IngestionServiceClient();

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

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

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

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

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

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

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

PHP

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Python

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

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

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

_logger = logging.getLogger(__name__)

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


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

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

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

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

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

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

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

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

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

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

        if user_data.user_identifiers:
            event.user_data = user_data

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

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

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

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

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

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

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

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


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


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

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

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

成功回應

如果要求成功,傳回的回應會包含 requestId 物件。如有任何選填欄位未通過驗證,回應也會包含 fieldWarnings 清單。

標準回覆

以下是成功擷取要求的回應範例,其中沒有警告:

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

回應 (含警告)

以下是成功擷取要求的回應範例,其中包含警告:

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

記錄傳回的 requestId,以便在處理要求中的每個目的地時,擷取診斷資訊。此外,您也應檢查是否有任何 fieldWarnings ,確保系統已接受您傳送的選填欄位。詳情請參閱「擷取警告」。

失敗回應

如果要求失敗,系統會傳回錯誤回應狀態碼 (例如 400 Bad Request),以及含有錯誤詳細資料的回應。

Data Manager API 採用快速失敗模式:只要有任何事件未通過驗證,整個要求就會失敗,且不會處理任何事件。請修正錯誤,然後重新傳送整個要求。如要在不擷取資料的情況下檢查這些錯誤,請傳送要求並將 validateOnly 設為 true。

舉例來說,如果 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 的事件,請設定每個 Destination 的 reference,在單一要求中傳送這兩個事件。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"
    }
  ]

設定每個 Event 的 destinationReferences,將其傳送至一或多個特定目的地。舉例來說,以下 Event 僅適用於第一個 Destination,因此其 destinationReferences 清單僅包含第一個 Destination 的 reference:

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

destinationReferences 欄位是清單,因此您可以為事件指定多個目的地。如未設定 Event 的 destinationReferences,Data Manager API 會將事件傳送至要求中的所有目的地。

如果事件有多個目的地,Data Manager API 會將相關欄位傳送至每個目的地。舉例來說,如果事件有 Google Ads 目的地和 Google Analytics 目的地,當 API 將事件傳送至 Google Analytics 目的地時,會加入 clientId、appInstanceId 或 eventName 等 Google Analytics 欄位;當 API 將事件傳送至 Google Ads 目的地時,則會加入 customVariables 等 Google Ads 欄位。

後續步驟