イベントを送信する

このクイックスタートでは、イベントデータの送信について説明します。

次のいずれかのシナリオで Data Manager API を使用します。

  • Google 広告タグのコンバージョンまたは Google アナリティクスの purchase イベントをタグのコンバージョンの追加のデータソースとして送信すると、広告インタラクション シグナルを最大限に活用し、データと全体的なパフォーマンスを強化できます。

    この機能はすべての Google 広告アカウントでご利用いただけますが、許可リストに登録されている Google アナリティクス プロパティでのみご利用いただけます。Google アナリティクス プロパティを許可リストに追加することにご関心がある場合は、こちらのフォームにご記入ください

  • Google 広告のオフライン コンバージョンまたはリードの拡張コンバージョンのイベントデータを送信します。

表示するガイドのバージョンを選択します。

  • 管理する広告主アカウントのユーザーである Google アカウントの認証情報を使用する場合は、[広告主] を選択します。
  • データ パートナー アカウントのユーザーである Google アカウントの認証情報を使用して、データ パートナー アカウントへのパートナー リンクがある広告主アカウントを管理する場合は、[データ パートナー] を選択します。

このクイックスタートでは、次の手順を行います。

  1. イベントデータを受信する Destination を準備します。
  2. 送信するイベントデータを準備します。
  3. イベントの IngestionService リクエストをビルドします。
  4. Google APIs Explorer でリクエストを送信します。
  5. 成功レスポンスと失敗レスポンスを理解する。

デスティネーションを準備する

データを送信する前に、データの Destination を少なくとも 1 つ準備する必要があります。使用できる 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 広告アカウントまたは Google アナリティクス プロパティのいずれかになります。

accountTypeGOOGLE_ANALYTICS_PROPERTY の場合、リクエストの認証情報は、プロパティの編集者または管理者ロールを持つ Google アナリティクス ユーザーのものである必要があります。

オフライン コンバージョンとリードの拡張コンバージョンでは、運用アカウントは Google 広告アカウントである必要があります。

loginAccount
認証情報の Google アカウントがユーザーであるアカウント。
productDestinationId

イベントを受け取る operatingAccount 内のエンティティの ID。

追加のデータソースとして送信されるイベントの場合、productDestinationId は次のいずれかである必要があります。

  1. typeWEBPAGE に設定されている Google 広告コンバージョンの ID。Google 広告の管理画面では、WEBPAGE コンバージョン アクションのコンバージョンの発生元は [ウェブサイト] になります。

  2. Google アナリティクスのウェブ ストリームの測定 ID。イベントを Google アナリティクスの iOS アプリまたは Android アプリのストリームに追加のデータソースとして送信することはできません。

オフライン コンバージョンまたはリードの拡張コンバージョンの場合、productDestinationIdtypeUPLOAD_CLICKS に設定されている Google 広告のコンバージョン アクションの ID である必要があります。Google 広告の管理画面では、UPLOAD_CLICKS コンバージョン アクションのコンバージョンの発生元は [ウェブサイト(クリックからのインポート)] になります。

このガイドの例では、すべてのイベントを同じ宛先に送信するリクエストを作成する方法を示します。同じリクエストで複数の宛先にイベントを送信する場合は、複数の宛先にイベントを送信するをご覧ください。

イベントデータを準備する

次のイベントデータを考えてみましょう。各表は 1 つのコンバージョン イベントに対応しています。各コンバージョン イベントには、イベントのタイムスタンプ、コンバージョン アクション、コンバージョン値が含まれます。

各イベントには、gclid などの広告 ID や、メールアドレス、電話番号、住所情報などのユーザー ID が含まれている場合があります。イベントには次のものを含めることもできます。

  • イベント発生時に評価されたユーザーに関する情報(顧客の価値、新規顧客、リピーター、再エンゲージメントした顧客かどうかなど)。
  • ショッピング カートのデータ。
  • Google アナリティクスの 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 アルゴリズムを使用してハッシュ化し、16 進数または Base64 エンコードを使用してエンコードする必要があります。16 進数エンコードを使用してフォーマット、ハッシュ化、エンコードを行った後のイベントデータは次のようになります。

イベント 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 アナリティクスのイベントには、過去 72 時間以内の eventTimestamp が必要です。

  2. ユースケースの必須フィールドを設定します。

    ユースケース 識別子 transactionId eventSource
    オフライン コンバージョンまたはリードの拡張コンバージョン 必須。次のうち少なくとも 1 つを設定します。 省略可 必須EventSource の列挙値のいずれかに設定します。
    Google 広告の宛先に追加のデータソースとして送信されたイベント 必須。次のうち少なくとも 1 つを設定します。 必須 省略可。設定する場合は、WEB にする必要があります。
    Google アナリティクスの宛先に追加のデータソースとして送信されるイベント 必須。次のうち少なくとも 1 つを設定します。 必須 省略可。設定する場合は、WEB にする必要があります。
  3. イベントを追加のデータソースとして Google 広告の宛先に送信する場合は、Google が追加のデータソースのデータを処理する方法をご確認ください。

  4. イベントの値があるその他のフィールドに入力します。使用可能なフィールドの完全なリストについては、Event リファレンス ドキュメントをご覧ください。

Google による補助データソースのデータの取り扱い

同じコンバージョン アクション内で、Google は transactionId を使用して、さまざまなソース(ウェブサイト タグや Data Manager API の取り込みリクエストなど)から送信されたコンバージョン イベントを重複除去します。次の表に、取り込みリクエストのデータがどのように処理されるかを示します。

シナリオ データ フィールド 処理方法
transactionId 既存のタグイベントと一致します conversionValuecurrencyCode を含む)

更新しました。EventconversionValuecurrencyCode を含む)は、タグによって記録された元の値をオーバーライドします。

注: コンバージョン アクションの最初の 14 日間のトライアル期間中は、値の更新は無効になります。トライアル期間が終了するまで、Google 広告のレポートでタグの値がオーバーライドされることはありません。

transactionId 既存のタグイベントと一致します conversionValue または currencyCode 以外のフィールド(adIdentifiers.gclid など) 無視されます。追加のデータソースの他のフィールドの値は、トランザクションが一致しても、Google タグによって最初に記録されたフィールドの値を上書きしません。
transactionId が既存のイベントと一致しない 提供されたすべてのデータ(userDataconversionValuecurrencyCode など)

新しいコンバージョン イベントの作成に使用されます。Google は、提供された識別子(adIdentifiers.gcliduserData など)を使用して、この新しいコンバージョンを広告クリックに関連付けようとします。

注: 最初の 14 日間のトライアル期間中、これらの新しいコンバージョンはレポートに表示されますが、入札には使用されません。トライアル期間が終了すると、自動的に入札対象になります。

セッション属性を追加する

オフライン コンバージョンまたはリードの拡張コンバージョンを送信する場合は、GCLID や WBRAID などの他の広告識別子が利用できないときに、セッション属性を追加します。他の広告 ID に加えて、セッション属性を含めることもできます。

セッション属性は、ウェブサイトでのユーザー インタラクションに関する追加のコンテキストとシグナルを提供します。これにより、コンバージョン測定、レポート作成、入札の精度を高めることができます。

Data Manager API では、セッション属性を送信する方法が 2 つあります。

  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 を一貫して送信します。これらのフィールドは、正確なアトリビューションに不可欠です。
    • プレースホルダ文字列、内部アプリケーション パス、不完全な URL など、不正確または部分的な landing_page_url 値は指定しないでください。正確な完全な URL がない場合は、landing_page_url を省略します。

    次に、gad_campaignidsession_start_time_usecexperimentalFields にエントリがあり、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 アナリティクスの情報を追加する

追加のデータソースとして送信されるイベントのリンク先に Google アナリティクス プロパティが含まれている場合は、次のフィールドに指定された値を入力します。

eventName

必須。Google アナリティクス イベントの名前。

transactionId

必須。イベントの一意の識別子。

少なくとも 1 つの識別子

次のフィールドのうち少なくとも 1 つを設定する必要があります。

destinationReferences

リクエスト レベルの destinations リストに複数の Google アナリティクス Destination が含まれている場合は必須です。destinationReferences にエントリを追加して、イベントを受信する Google アナリティクスのリンク先を指定します。宛先参照の詳細については、複数の宛先にイベントを送信するをご覧ください。

destinationReferences が設定されていない場合や、Google アナリティクスの宛先を参照するエントリが複数ある場合、Data Manager API はエラー MULTIPLE_DESTINATIONS_FOR_GOOGLE_ANALYTICS_EVENT でイベントを拒否します。

userId

省略可。ユーザーの User-ID

additionalEventParameters

これは省略可能ですが、指定することをおすすめします。このリストには、他の Event フィールドで取得されない Google アナリティクス イベント パラメータを入力します。パラメータには、purchase イベントから推奨される追加のパラメータや、キャプチャするその他のパラメータを含めることができます。EventParameterparameterName には、Google アナリティクスのパラメータ名を使用します。

たとえば、取引に関連付けられた税金がある場合は、parameterNametax に設定し、value を税額に設定して、additionalEventParameters にエントリを追加します。

transactionIdcurrencyvalue の Google アナリティクス イベント パラメータのエントリを追加することはおすすめしません。代わりに、EventtransactionIdcurrencyconversionValue に値を入力します。これらの値は additionalEventParameters のエントリよりも優先されます。

購入イベントのカートデータを追加する

EventcartData フィールドに、購入したアイテムに関する情報を入力します。購入されたアイテムごとに、CartDataitems リストに Item オブジェクトを追加し、次のフィールドに指定された値を入力します。

itemId
必須。アイテムの一意の識別子。
unitPrice

必須。税金、送料、イベント スコープ(トランザクション単位)の割引を除いた単価。

アイテムにアイテム スコープの割引がある場合は、割引単価を使用します。たとえば、商品の単価が 27.67 で、単位割引が 6.66 の場合、unitPrice21.01 に設定します。

quantity

必須。この特定の商品アイテムの購入数。

additionalItemParameters

このリストには、他の Item フィールドで取得されないアイテム スコープのパラメータを入力します。ItemParameterparameterName には、Google アナリティクス アイテム パラメータ名を使用します。

たとえば、アイテムのブランドとカテゴリがある場合は、parameterNameitem_brand に、value をブランド名に設定したエントリと、parameterNameitem_category に、value をアイテムのカテゴリに設定したエントリを、アイテムの additionalItemParameters に追加します。

quantitypriceitem_id の Google アナリティクス アイテム パラメータのエントリを追加することはおすすめしません。代わりに、ItemitemIdunitPricequantity に値を入力します。これらの値は additionalItemParameters のエントリよりも優先されます。

以下は、2 つ目のイベントのフォーマット、ハッシュ化、エンコードされたデータの Event の例です。Google アナリティクスの追加データも含まれています。

{
  "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] タブを選択し、[API Explorer で開く] をクリックして、新しいタブまたはウィンドウで API Explorer を開きます。
  2. API エクスプローラの本文で、REPLACE_WITH で始まる各文字列(REPLACE_WITH_OPERATING_ACCOUNT_TYPE など)を関連する値に置き換えます。
  3. API Explorer ページの下部にある [実行] をクリックし、認証プロンプトを完了してリクエストを送信します。
  4. validateOnlytrue に設定して、変更を適用せずにリクエストを検証します。変更を適用する準備ができたら、validateOnlyfalse に設定します。

クライアント ライブラリをインストールした場合は、選択したプログラミング言語のタブを選択して、リクエストの作成と送信方法の完全なコードサンプルをご覧ください。

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 などのエラー レスポンス ステータス コードと、エラーの詳細を含むレスポンスが返されます。

たとえば、16 進数エンコード値ではなくプレーン テキスト文字列を含む 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"
          }
        ]
      }
    ]
  }
}

ハッシュ化されず、16 進数エンコードのみが行われた 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 を設定して、両方のイベントを 1 つのリクエストで送信します。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 を設定して、1 つ以上の特定の宛先に送信します。たとえば、最初の Destination のみの Event は次のようになります。この場合、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 広告と Google アナリティクスの両方である場合、API は、Google アナリティクスの宛先にイベントを送信するときに clientIdeventName などの Google アナリティクスのフィールドを含め、Google 広告の宛先にイベントを送信するときに customVariables などの Google 広告のフィールドを含めます。

次のステップ