ارسال رویدادها

این راهنما را دنبال کنید تا با فرایند ارسال تبدیل‌های آفلاین Google Ads یا تبدیل‌های بهبودیافته برای مشتریان بالقوه با Data Manager API آشنا شوید.

در این راهنما، مراحل زیر را تکمیل می‌کنید:

  1. Destination را برای دریافت داده‌های رویداد آماده کنید.
  2. داده‌های رویداد را برای ارسال آماده کنید.
  3. درخواست IngestionService برای رویدادها ایجاد کنید.
  4. درخواست را با Google APIs Explorer ارسال کنید.
  5. پاسخ‌های موفقیت و شکست را درک کنید.

آماده کردن مقصدها

قبل‌از اینکه بتوانید داده ارسال کنید، باید حداقل یک Destination برای داده‌ها آماده کنید. فیلدهای Destination در اینجا آمده است. برای جزئیات بیشتر و نمونه‌هایی از مقصدها برای سناریوهای مختلف، پیکربندی مقصدها را بررسی کنید.

برگه مربوط به مورد استفاده‌تان را انتخاب کنید.

  • اگر از اطلاعات اعتباری «حساب Google» استفاده می‌کنید که کاربر در حساب‌های تبلیغ‌کننده‌ای است که می‌خواهید مدیریت کنید، تبلیغ‌کننده را انتخاب کنید.
  • اگر از اطلاعات اعتباری «حساب Google» استفاده می‌کنید که کاربر حساب شریک داده است و می‌خواهید حساب‌های تبلیغ‌کننده‌ای را که پیوند شریک به حساب شریک داده دارند مدیریت کنید، شریک داده را انتخاب کنید. حساب‌های شریک داده فقط پس‌از طی کردن فرایند تأیید صادر می‌شوند. برای شروع، فرم علاقه‌مندی را تکمیل کنید.

آگهی‌دهنده

operatingAccount

حسابی که رویدادها را دریافت می‌کند. باید حساب Google Ads باشد که مالک کنش تبدیل است.

accountType را روی GOOGLE_ADS و accountId را روی شناسه مشتری ۱۰ رقمی Google Ads (بدون خط تیره) تنظیم کنید.

loginAccount

حسابی که در آن «حساب Google» مربوط به اطلاعات اعتباری کاربر است.

productDestinationId

شناسه کنش تبدیل Google Ads در operatingAccount که رویدادها را دریافت می‌کند.

کنش تبدیل باید type روی UPLOAD_CLICKS تنظیم شده باشد. در میانای کاربری Google Ads، منبع تبدیل برای کنش تبدیل UPLOAD_CLICKS وب‌سایت (وارد کردن از کلیک‌ها) است.

{
  "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 را روی شناسه مشتری ۱۰ رقمی Google Ads خود (بدون خط تیره) تنظیم کنید.

loginAccount

حسابی که کاربر اطلاعات کاربری به آن دسترسی دارد.

accountId را روی شناسه حساب شریک داده خود تنظیم کنید و accountType را روی DATA_PARTNER تنظیم کنید. شناسه حساب شریک داده شما پس‌از تأیید درخواست شریکتان توسط Google ارائه می‌شود.

linkedAccount

حساب با پیوند شریک ایجادشده که ازطریق آن کاربر اعتبارنامه به operatingAccount دسترسی دارد.

اگر ولی operatingAccount به حساب شریک داده شما پیوند شده است، linkedAccount را روی ولی operatingAccount تنظیم کنید. اگر operatingAccount مستقیماً به حساب شریک داده شما پیوند داده شده است، linkedAccount را تنظیم نکنید.

productDestinationId

شناسه کنش تبدیل Google Ads در operatingAccount که رویدادها را دریافت می‌کند.

کنش تبدیل باید type روی UPLOAD_CLICKS تنظیم شده باشد. در میانای کاربری Google Ads، منبع تبدیل برای کنش تبدیل UPLOAD_CLICKS وب‌سایت (وارد کردن از کلیک‌ها) است.

{
  "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. شناسه‌های کاربر مثل نشانی‌های ایمیل، نام‌های کوچک، و نام‌های خانوادگی باید بااستفاده از الگوریتم SHA-256 درهم‌سازی شوند و بااستفاده از درهم‌سازی هگزادسیمال (هگز) یا 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 (trimmed, lowercase) 8E621E3D0368631D263D07A351FA8D34FBA0D17C15FBCDEC11A5F58008D022A0 (هگز SHA-256)
phone_number +1 (800) 555-0199 +18005550199 (E.164) 78724165620B8A76386CAB242569EBC85DD2346F3F5AE88DC13BB57E31020B90 (هگز SHA-256)
given_name John john (trimmed, lowercase) 96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A (SHA-256 hex)
family_name Smith-Jones smith-jones (trimmed, lowercase) 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 اختیاری شناسه یکتای رویداد تبدیل که برای حذف موارد تکراری استفاده می‌شود.
eventSource الزامی منشأ رویداد. روی مقدار enum معتبر EventSource (مثل WEB یا CALL) تنظیم کنید.
conversionValue اختیاری ارزش پولی مرتبط با تبدیل.
currency اختیاری کد ارز ۳ حرفی ISO 4217 (مانند USD یا EUR) مرتبط با مقادیر پولی در این رویداد.
cartData اختیاری سبد خرید مرتبط و داده‌های خرید در سطح کالا. اگر ارائه شود، حداقل یک مورد در items الزامی است. افزودن داده‌های سبد خرید را ببینید.
destinationReferences الزامی مشروط فقط زمانی لازم است که رویدادها را در یک درخواست به چند مقصد ارسال کنید.
userProperties اختیاری اطلاعات مربوط به کاربر ارزیابی‌شده در زمان رویداد، مثل customerType (NEW یا RETURNING) یا customerValueBucket.
userId اختیاری شناسه یکتا برای کاربر.
consent اختیاری تنظیمات رضایت قانون بازارهای دیجیتال (DMA) برای کاربر، که مشخص می‌کند آیا رضایت برای adUserData و adPersonalization اعطا شده است یا نه.

الزامات شناسه

الزامی. باید حداقل یکی از موارد زیر را تنظیم کنید:

نحوه مدیریت داده‌های چندمنبعی توسط Google

در همان کنش تبدیل، Google Ads از transactionId برای حذف رویدادهای تبدیل تکراری که از منابع مختلف (مثل برچسب وب‌سایت شما و درخواست‌های انتقال «میانای برنامه‌سازی کاربردی مدیر داده‌ها») ارسال شده‌اند استفاده می‌کند. جدول زیر توضیح می‌دهد که چگونه داده‌های درخواست‌های انتقال شما پردازش می‌شود.

سناریو فیلد داده نحوه مدیریت
transactionId با رویداد برچسب موجود مطابقت دارد ‫conversionValue (با currency)

به‌روز شد. conversionValue (با currency) از Event مقدار اصلی ثبت‌شده توسط برچسب را لغو می‌کند.

توجه: درطول دوره آزمایشی اولیه ۱۴ روزه برای کنش تبدیل، به‌روزرسانی‌های مقدار غیرفعال است. تا زمانی که دوره آزمایشی به‌پایان نرسیده است، مقدار برچسب در گزارش‌های Google Ads ملغی نخواهد شد.

transactionId با رویداد برچسب موجود مطابقت دارد داده‌های ارائه‌شده کاربر در userData، مثل نشانی ایمیل، شماره تلفن، یا نشانی خیابان.
  • درصورت نبود تکمیل می‌شود: اگر برچسب Google تبدیل را ضبط کرده باشد اما نتوانسته باشد داده‌های ارائه‌شده توسط کاربر را ضبط کند، سیستم داده‌های ارائه‌شده توسط بارگذاری آفلاین شما را با موفقیت درج خواهد کرد.

  • اگر داده‌های برچسب وجود داشته باشد، نادیده گرفته می‌شود: اگر برچسب Google با موفقیت داده‌های ارائه‌شده توسط کاربر را در سایت ضبط کند، هرگونه داده بعدی یا متفاوتی که به‌صورت آفلاین برای همان تراکنش بارگذاری شود نادیده گرفته می‌شود (داده‌های برچسب موجود را بازنویسی یا به آن اضافه نمی‌کند).

‫transactionId با رویداد برچسب موجود مطابقت دارد فیلدهای دیگر به‌جز conversionValue، currency، یا userData (برای مثال، adIdentifiers.gclid) نادیده گرفته شد. مقادیر فیلد دیگر از منبع داده اضافی شما مقادیر فیلدی را که دراصل توسط برچسب Google شما برای تراکنش‌های منطبق ثبت شده است ملغی نمی‌کند.
transactionId با هیچ رویداد موجودی مطابقت ندارد همه داده‌های ارائه‌شده (برای نمونه، userData، conversionValue، currency)

برای ایجاد رویداد تبدیل جدید استفاده می‌شود. سپس Google تلاش می‌کند این تبدیل جدید را بااستفاده از شناسه‌هایی که ارائه کرده‌اید (مثل adIdentifiers.gclid یا userData) به کلیک آگهی نسبت دهد.

توجه: درطول دوره آزمایشی ۱۴ روزه اولیه، این تبدیل‌های جدیداً ایجادشده در گزارش‌دهی شما نشان داده خواهند شد اما برای پیشنهاد قیمت استفاده نخواهند شد. پس‌از پایان دوره آزمایشی، به‌طور خودکار پیشنهاد قیمت‌دادنی خواهند شد.

افزودن شاخصه‌های جلسه

وقتی شناسه‌های آگهی دیگر مثل GCLID یا WBRAID دردسترس نیست، مشخصه‌های جلسه را اضافه کنید. علاوه‌بر شناسه‌های آگهی دیگر، می‌توانید مشخصه‌های جلسه را نیز اضافه کنید.

شاخصه‌های جلسه بافت و نشان‌های بیشتری درباره تعامل کاربر با وب‌سایت شما ارائه می‌دهند که می‌تواند سنجش تبدیل، گزارش‌دهی، و دقت پیشنهاد قیمت را بهبود بخشد.

رویکردی را برای ارسال مشخصه‌های جلسه انتخاب کنید:

  • توصیه‌شده: فیلد sessionAttributes adIdentifiers را روی رشته مشخصه‌های جلسه کدبندی‌شده base64 ضبط‌شده در ارسال فرم تنظیم کنید.

  • جایگزین: فیلدهای شاخصه جلسه تکی را ضبط کنید و هر شاخصه را به رویداد در فیلدهای experimentalFields و adIdentifiers اضافه کنید. فقط درصورتی از این گزینه استفاده کنید که نتوانید رشته ویژگی‌های جلسه کدبندی‌شده را ضبط کنید.

فیلد sessionAttributes adIdentifiers را روی رشته مشخصه‌های جلسه کدبندی‌شده با base64 تنظیم کنید. دستورالعمل‌های نحوه ضبط session_attributes را دنبال کنید تا صفحه‌های ارسال فرم را برای ضبط رشته کدگذاری‌شده اصلاح کنید.

در اینجا بخشی از یک رویداد نمونه با مشخصه‌های جلسه کدبندی‌شده در فیلد sessionAttributes آمده است:

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

آلترناتیو

اگر نمی‌توانید رشته شاخصه‌های جلسه کدگذاری‌شده را ضبط کنید، ExperimentalField را برای هر شاخصه جلسه به فهرست experimentalFields اضافه کنید. علاوه‌براین، عامل کاربر را به landingPageDeviceInfo فیلد adIdentifiers فیلد اضافه کنید.

experimentalFields:

الزامی: برای هر کلید الزامی، ورودی جفت کلید-مقدار را به experimentalFields اضافه کنید:

  • gad_source:
  • gad_campaignid
  • session_start_time_usec

علاوه‌براین، می‌توانید یک جفت کلید-مقدار برای هر کلید اختیاری اضافه کنید:

  • landing_page_url

  • landing_page_referrer

adIdentifiers:

الزامی: نماینده کاربر صفحه مقصد را در فیلد userAgent adIdentifiers.landingPageDeviceInfo وارد کنید.

در اینجا بخشی از یک رویداد نمونه با ورودی‌های 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"
    }
  }
}

افزودن داده‌های سبد خرید

فیلد cartData از Event را با اطلاعات مربوط به موارد مرتبط با رویداد پر کنید. از این فیلد هنگام ارسال تبدیل‌های Google Ads با پارامترهای داده‌های سبد خرید استفاده کنید.

فیلدهای شیء CartData در اینجا آمده است:

items
الزامی. حداقل یک مورد در این فهرست اضافه کنید.

فیلدهای Merchant Center

فیلدهای merchantId، merchantFeedLabel، و merchantFeedLanguageCode اختیاری هستند. اگر موارد موجود در رویداد شامل محصولاتی است که در چندین حساب Merchant Center وجود دارند، این فیلدها را تنظیم کنید.

merchantId
‫
اختیاری. شناسه حساب Merchant Center.
merchantFeedLabel
اختیاری. برچسب فید فید Merchant Center. برچسب‌های فید به شما امکان می‌دهند محصولات را برای هدف‌یابی کمپین دسته‌بندی کنید. برای مثال، می‌توانید از برچسب‌های فید برای سازمان‌دهی محصولات براساس زبان استفاده کنید. اگر پویش شما محصولات را براساس کشور هدف‌یابی می‌کند، از کد کشور دوحرفی در قالب ISO 3166-1 alpha-2 (برای نمونه، US) استفاده کنید.
merchantFeedLanguageCode
اختیاری. کد زبان ISO 639-1 برای فید Merchant Center که حاوی موارد شما است (برای مثال، en).

فیلدهای مورد

یک یا چند شیء Item به فهرست items CartData اضافه کنید. وقتی cartData ارائه می‌شود، فهرست items نباید خالی باشد.

فیلدهای زیر را برای هر Item پر کنید:

items.itemId
الزامی. شناسه یکتای مورد.
items.merchantProductId
الزامی. شناسه محصول در حساب Merchant Center.
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 را انتخاب کنید و روی باز کردن در API Explorer کلیک کنید تا API Explorer در برگه یا پنجره جدیدی باز شود.
  2. در بدنه درخواست در «کاوشگر API»، هر رشته‌ای را که با REPLACE_WITH شروع می‌شود، مثل REPLACE_WITH_OPERATING_ACCOUNT_TYPE، با مقدار مربوطه جایگزین کنید.
  3. در پایین صفحه «کاوشگر API»، روی اجرا کلیک کنید و پیام‌واره‌های مجوز را برای ارسال درخواست تکمیل کنید.
  4. validateOnly را روی true تنظیم کنید تا درخواست بدون اعمال تغییرات اعتبارسنجی شود. وقتی برای اعمال تغییرات آماده بودید، validateOnly را روی false تنظیم کنید.

اگر کتابخانه مشتری نصب کرده‌اید، برگه زبان برنامه‌نویسی انتخابی‌تان را انتخاب کنید تا نمونه کد کاملی از نحوه ساختن و ارسال درخواست ببینید.

استراحت

آگهی‌دهنده

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

دات نت

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

جاوا

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

Node

#!/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"
          }
        ]
      }
    ]
  }
}

ارسال رویدادها برای چند مقصد

اگر داده‌هایتان حاوی رویدادهایی برای مقصدهای مختلف است، می‌توانید آن‌ها را در همان درخواست بااستفاده از مرجع‌های مقصد ارسال کنید. برای اطلاع از حداکثر تعداد مقصدها در هر درخواست، محدودیت‌ها و سهمیه‌ها را ببینید.

برای مثال، اگر رویدادی برای شناسه کنش تبدیل 123456789 و رویداد دیگری برای شناسه کنش تبدیل 777111122 دارید، هر دو رویداد را در یک درخواست واحد با تنظیم reference هریک از Destination ارسال کنید. 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"
    }
  ]

destinationReferences هر Event را تنظیم کنید تا آن را به یک یا چند مقصد خاص ارسال کنید. برای مثال، در اینجا Event وجود دارد که فقط برای Destination اول است، بنابراین فهرست destinationReferences آن فقط شامل reference Destination اول است:

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

فیلد destinationReferences یک فهرست است، بنابراین می‌توانید چندین مقصد را برای یک رویداد مشخص کنید. اگر destinationReferences Event را تنظیم نکنید، Data Manager API رویداد را به همه مقصدهای موجود در درخواست ارسال می‌کند.

اگر رویدادی چندین مقصد داشته باشد، «میانای برنامه‌سازی کاربردی مدیر داده‌ها» فیلدهای مربوطه را به هر مقصد ارسال می‌کند. برای مثال، اگر رویدادی مقصد Google Ads و مقصد Google Analytics داشته باشد، وقتی رویداد به مقصد Google Analytics ارسال می‌شود، API شامل فیلدهای Google Analytics مثل clientId، appInstanceId، یا eventName می‌شود، و وقتی رویداد به مقصد Google Ads ارسال می‌شود، شامل فیلدهای Google Ads مثل customVariables می‌شود.

مراحل بعدی