ارسال اعضای مخاطب

برای آشنایی با فرایند ارسال اعضای مخاطب با «میانای برنامه‌سازی کاربردی مدیر داده»، این راهنما را دنبال کنید.

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

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

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

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

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

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

آگهی‌دهنده

operatingAccount

حسابی که داده‌های مخاطب را دریافت می‌کند. این را روی حساب تبلیغ‌کننده‌ای که مالک مخاطب است تنظیم کنید.

loginAccount

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

اگر اطلاعات اعتباری OAuth شما برای کاربری است که به حساب مدیر Google Ads دسترسی دارد و operatingAccount یکی از حساب‌های فرعی آن است، loginAccount را روی نوع حساب و شناسه حساب مدیر تنظیم کنید.

اگر اطلاعات اعتباری OAuth برای کاربری باشد که به operatingAccount دسترسی مستقیم دارد، نیازی نیست loginAccount را تنظیم کنید.

productDestinationId

شناسه مخاطب در operatingAccount که داده‌های عضو را دریافت می‌کند.

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

شریک داده

operatingAccount

نوع حساب و شناسه حسابی که داده‌های مخاطب را دریافت می‌کند. این را روی حساب تبلیغ‌کننده‌ای که مالک مخاطب است تنظیم کنید.

loginAccount

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

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

linkedAccount

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

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

productDestinationId

شناسه مخاطب در operatingAccount که داده‌های عضو را دریافت می‌کند.

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

آماده کردن داده‌های مخاطبان

داده‌های نمونه زیر را درنظر بگیرید. هر جدول مربوط به یکی از اعضای مخاطب است. هر عضو می‌تواند حداکثر سه نشانی ایمیل داشته باشد و همچنین ممکن است اطلاعات نشانی IP یا شماره تلفن داشته باشد.

در اینجا داده‌های عضو مخاطب آمده است:

عضو ۱

عضو شماره ۱
email_1 dana@example.com
email_2 DanaM@example.com
email_3
ip_1 192.0.2.211
ip_2 198.51.100.14

عضو ۲

عضو شماره ۲
email_1 ALEXF@example.com
email_2 AlexF@cymbalgroup.com
email_3 alexF@altostrat.com
phone_1 +1-800-555-0100
phone_2 +1 800-555-0101
phone_3 +1 800-555-0102

عضو ۳

عضو شماره ۳
email_1 quinn@CYMBALGROUP.com
email_2 baklavainthebalkans@gmail.com
email_3
ip_1 203.0.113.98
ip_1_start_time Jun 10, 2026 8:17:52 PM EDT
ip_1_end_time Jun 17, 2026 4:02:04.123 AM EDT

عضو ۴

عضو شماره ۴
email_1 rosario@example.org
email_2 cloudySanFrancisco@GMAIL.com
email_3
phone_1 +1-800-555-0110

قالب‌بندی داده‌ها

فیلدها را طبق آنچه در راهنمای قالب‌بندی مشخص شده است قالب‌بندی کنید.

داده‌های مخاطب پس‌از قالب‌بندی در اینجا آمده است:

عضو ۱

عضو شماره ۱
email_1 dana@example.com
email_2 danam@example.com
email_3
ip_1 192.0.2.211
ip_2 198.51.100.14

عضو ۲

عضو شماره ۲
email_1 alexf@example.com
email_2 alexf@cymbalgroup.com
email_3 alexf@altostrat.com
phone_1 +18005550100
phone_2 +18005550101
phone_3 +18005550102

عضو ۳

عضو شماره ۳
email_1 quinn@cymbalgroup.com
email_2 baklavainthebalkans@gmail.com
email_3
ip_1 203.0.113.98
ip_1_start_time 2026-06-10T20:17:52.0-04:00
ip_1_end_time 2026-06-17T04:02:04.123-04:00

عضو ۴

عضو شماره ۴
email_1 rosario@example.org
email_2 cloudysanfrancisco@gmail.com
email_3
phone_1 +18005550110

داده‌ها را درهم‌سازی و کدبندی کنید

علاوه‌براین، نشانی‌های ایمیل و شماره تلفن‌های قالب‌بندی‌شده باید بااستفاده از الگوریتم SHA-256 درهم‌سازی شوند و بااستفاده از دستگاه شانزده‌شانزدهی (هگز) یا کدبندی Base64 کدبندی شوند. مثال‌های این راهنما از کدبندی برمبنای شانزده استفاده می‌کنند.

نشانی‌های IP یا مُهرهای زمان را درهم‌سازی نکنید.

داده‌ها پس‌از درهم‌سازی و کدبندی در اینجا آمده است:

عضو ۱

عضو شماره ۱
email_1 07e2f1394b0ea80e2adca010ea8318df697001a005ba7452720edda4b0ce57b3
email_2 1df6b43bc68dd38eca94e6a65b4f466ae537b796c81a526918b40ac4a7b906c7
email_3
ip_1 192.0.2.211
ip_2 198.51.100.14

عضو ۲

عضو شماره ۲
email_1 19de0a9906ebee29fe82087be5ec44f3c01e40730bc808b663c71f19dd6b12f2
email_2 7d2d80e240594dcee2e57f57885ee286c4b232dd142234d7a0af6d31cc86679b
email_3 063c45291827d4994f98357b72862b25885834fd53cf8f0e4af359916fc03c62
phone_1 fb4f73a6ec5fdb7077d564cdd22c3554b43ce49168550c3b12c547b78c517b30
phone_2 61d6c2976b5d92b1ad63073a12fddd26040cd980067d03a93dd1cc50f1a3ac68
phone_3 c5383c2eeada28210d27f011bc127d4f9562cf38ff0c9bba33dd45ae79b9fe7c

عضو ۳

عضو شماره ۳
email_1 05bb62526f091b45d20e243d194766cca8869137421047dc53fa4876d111a6f0
email_2 f1fcde379f31f4d446b76ee8f34860eca2288adc6b6d6c0fdc56d9eee75a2fa5
email_3
ip_1 203.0.113.98
ip_1_start_time 2026-06-10T20:17:52.0-04:00
ip_1_end_time 2026-06-17T04:02:04.123-04:00

عضو ۴

عضو شماره ۴
email_1 83a834cc5327bc4dee7c5408988040dc5813c7662611cd93b707aff72bf7d33f
email_2 223ebda6f6889b1494551ba902d9d381daf2f642bae055888e96343d53e9f9c4
email_3
phone_1 525c2390a4e2a9766fa8ddc341dd10414603baebefa401f5a5e29f55e176525d

در اینجا AudienceMember نمونه‌ای برای عضو اول در داده‌های نمونه نشان داده شده است، که نشانی‌های ایمیل قالب‌بندی‌شده، درهم‌سازی‌شده، و کدبندی‌شده، و نشانی‌های IP درهم‌سازی‌نشده را نشان می‌دهد:

{
  "compositeData": {
    "userData": {
      "userIdentifiers": [
        {
          "emailAddress": "07e2f1394b0ea80e2adca010ea8318df697001a005ba7452720edda4b0ce57b3"
        },
        {
          "emailAddress": "1df6b43bc68dd38eca94e6a65b4f466ae537b796c81a526918b40ac4a7b906c7"
        }
      ]
    },
    "ipData": [
      {
        "ipAddress": "192.0.2.211"
      },
      {
        "ipAddress": "198.51.100.14"
      }
    ]
  }
}

داده‌های ترکیبی

دو روش برای ارائه شناسه‌های کاربر برای بارگذاری AudienceMember وجود دارد. باید از رویکرد یکسانی برای هر AudienceMember در درخواست استفاده کنید.

  1. compositeData (توصیه‌شده)

    این رویکرد برای همه یکپارچه‌سازی‌ها توصیه می‌شود.

    فیلد compositeData را در AudienceMember روی CompositeData شیء تنظیم کنید که فیلد userData تودرتویی دارد که در آن می‌توان شناسه‌های کاربر را ارائه کرد. عملکرد فیلد userData در compositeData با عملکرد فیلد userData در AudienceMember یکسان است.

    CompositeData می‌تواند شامل یکی از موارد زیر باشد:

  2. userData

    همچنین می‌توانید شناسه‌های کاربر را با تنظیم مستقیم فیلد userData در AudienceMember روی شیء UserData ارائه دهید.

    درحالی‌که userData همچنان پشتیبانی می‌شود، توصیه می‌کنیم از compositeData استفاده کنید تا مطمئن شوید یکپارچه‌سازی شما برای بهبودهای آینده آماده است.

انتقال داده‌های IP برای «مطابقت مشتری» Google Ads

‫Data Manager API از افزودن نشانی‌های IP برای بارگذاری‌های «مطابقت مشتری Google Ads» در CompositeData پشتیبانی می‌کند. برای هر نشانی IP، منبع IpData را به ipData فیلد تکرارشونده اضافه کنید. ارسال IpData برای بارگذاری‌های «تطابق مشتری» در Display & Video 360 پشتیبانی نمی‌شود.

IpData حاوی فیلد ipAddress الزامی و دو فیلد اختیاری مُهر زمان observeStartTime و observeEndTime است که نشان می‌دهد نشانی IP چه زمانی برای کاربر مشاهده شده است. توصیه می‌کنیم این فیلدهای مُهر زمان را اضافه کنید تا به بهبود نرخ‌های مطابقت «مطابقت مشتری» کمک کنید.

در اینجا رهنمودهایی برای افزودن نشانی‌های IP آورده شده است:

  • هم از IPv4 و هم از IPv6 پشتیبانی می‌شود.
  • این فیلدها را درهم‌سازی یا کدبندی نکنید.
  • برای جزئیات مربوط به نحوه قالب‌بندی ipAddress و فیلدهای مُهر زمان، به راهنمای قالب‌بندی مراجعه کنید.
  • هر شیء در فهرست ipData باید مقدار غیرخالی برای فیلد ipAddress داشته باشد. توصیه می‌کنیم observeStartTime و observeEndTime را نیز ارسال کنید.
  • با تکمیل کردن هر دو userData و ipData در CompositeData می‌توانید شناسه‌های کاربر و داده‌های نشانی IP را در بارگذاری‌های «مطابقت مشتری» Google Ads ارسال کنید. مطمئن شوید userData حداقل یک UserIdentifier داشته باشد.
  • مخاطب مقصد باید CONTACT_ID را در uploadKeyTypes خود داشته باشد. درخواست‌های ارسال compositeData به مخاطبانی که این نوع کلید را ندارند رد خواهد شد.

در اینجا شیء AudienceMember برای عضو سوم داده‌های نمونه نشان داده شده است که نحوه افزودن userData و ipData (ازجمله مُهرهای زمان مشاهده) را که در فیلد compositeData تودرتو شده‌اند نشان می‌دهد.

{
  "compositeData": {
    "userData": {
      "userIdentifiers": [
        {
          "emailAddress": "05bb62526f091b45d20e243d194766cca8869137421047dc53fa4876d111a6f0"
        },
        {
          "emailAddress": "f1fcde379f31f4d446b76ee8f34860eca2288adc6b6d6c0fdc56d9eee75a2fa5"
        }
      ]
    },
    "ipData": [
      {
        "ipAddress": "203.0.113.98",
        "observeStartTime": "2026-06-10T20:17:52.0-04:00",
        "observeEndTime": "2026-06-17T04:02:04.123-04:00"
      }
    ]
  }
}

ساختن متن درخواست

برای ساختن متن درخواست، destinations و audienceMembers را ترکیب کنید، فیلد encoding را تنظیم کنید، و هر فیلد درخواست دیگری را که می‌خواهید اضافه کنید، مثل validateOnly و consent.

اگر اعضای مخاطب را برای «تطابق مشتری» ارسال می‌کنید، termsOfService را تنظیم کنید تا نشان دهد کاربر شرایط خدمات «تطابق مشتری» را پذیرفته است یا نه.

مثال‌های این راهنما از رمزگذاری استفاده نمی‌کنند، اما می‌توانید با دنبال کردن دستورالعمل‌های رمزگذاری داده‌های کاربر، رمزگذاری را به فرایندتان اضافه کنید.

ارسال درخواست

در اینجا مراحل امتحان کردن درخواست از مرورگرتان آمده است:

  1. برگه REST را انتخاب کنید و روی باز کردن در «کاوشگر API» کلیک کنید تا «کاوشگر API» در برگه یا پنجره جدیدی باز شود.
  2. در بدنه درخواست در «کاوشگر API»، هر رشته‌ای را که با REPLACE_WITH شروع می‌شود، مثل REPLACE_WITH_OPERATING_ACCOUNT_TYPE، با مقدار مربوطه جایگزین کنید.
  3. در پایین صفحه «کاوشگر API»، روی اجرا کلیک کنید و پیام‌واره‌های مجوز را برای ارسال درخواست تکمیل کنید.
  4. ‫validateOnly را روی true تنظیم کنید تا درخواست را بدون اعمال تغییرات اعتبارسنجی کنید. وقتی آماده اعمال تغییرات بودید، validateOnly را روی false تنظیم کنید.

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

REST

آگهی‌دهنده

{
    "destinations": [
        {
            "operatingAccount": {
                "accountType": "OPERATING_ACCOUNT_TYPE",
                "accountId": "OPERATING_ACCOUNT_ID"
            },
            "loginAccount": {
                "accountType": "LOGIN_ACCOUNT_TYPE",
                "accountId": "LOGIN_ACCOUNT_ID"
            },
            "productDestinationId": "AUDIENCE_ID"
        }
    ],
    "audienceMembers": [
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "07e2f1394b0ea80e2adca010ea8318df697001a005ba7452720edda4b0ce57b3"
                        },
                        {
                            "emailAddress": "1df6b43bc68dd38eca94e6a65b4f466ae537b796c81a526918b40ac4a7b906c7"
                        }
                    ]
                },
                "ipData": [
                    {
                        "ipAddress": "192.0.2.211"
                    },
                    {
                        "ipAddress": "198.51.100.14"
                    }
                ]
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "19de0a9906ebee29fe82087be5ec44f3c01e40730bc808b663c71f19dd6b12f2"
                        },
                        {
                            "emailAddress": "7d2d80e240594dcee2e57f57885ee286c4b232dd142234d7a0af6d31cc86679b"
                        },
                        {
                            "emailAddress": "063c45291827d4994f98357b72862b25885834fd53cf8f0e4af359916fc03c62"
                        },
                        {
                            "phoneNumber": "fb4f73a6ec5fdb7077d564cdd22c3554b43ce49168550c3b12c547b78c517b30"
                        },
                        {
                            "phoneNumber": "61d6c2976b5d92b1ad63073a12fddd26040cd980067d03a93dd1cc50f1a3ac68"
                        },
                        {
                            "phoneNumber": "c5383c2eeada28210d27f011bc127d4f9562cf38ff0c9bba33dd45ae79b9fe7c"
                        }
                    ]
                }
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "05bb62526f091b45d20e243d194766cca8869137421047dc53fa4876d111a6f0"
                        },
                        {
                            "emailAddress": "f1fcde379f31f4d446b76ee8f34860eca2288adc6b6d6c0fdc56d9eee75a2fa5"
                        }
                    ]
                },
                "ipData": [
                    {
                        "ipAddress": "203.0.113.98",
                        "observeStartTime": "2026-06-10T20:17:52.0-04:00",
                        "observeEndTime": "2026-06-17T04:02:04.123-04:00"
                    }
                ]
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "83a834cc5327bc4dee7c5408988040dc5813c7662611cd93b707aff72bf7d33f"
                        },
                        {
                            "emailAddress": "223ebda6f6889b1494551ba902d9d381daf2f642bae055888e96343d53e9f9c4"
                        },
                        {
                            "phoneNumber": "525c2390a4e2a9766fa8ddc341dd10414603baebefa401f5a5e29f55e176525d"
                        }
                    ]
                }
            }
        }
    ],
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "encoding": "HEX",
    "termsOfService": {
        "customerMatchTermsOfServiceStatus": "ACCEPTED"
    },
    "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": "AUDIENCE_ID"
        }
    ],
    "audienceMembers": [
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "07e2f1394b0ea80e2adca010ea8318df697001a005ba7452720edda4b0ce57b3"
                        },
                        {
                            "emailAddress": "1df6b43bc68dd38eca94e6a65b4f466ae537b796c81a526918b40ac4a7b906c7"
                        }
                    ]
                },
                "ipData": [
                    {
                        "ipAddress": "192.0.2.211"
                    },
                    {
                        "ipAddress": "198.51.100.14"
                    }
                ]
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "19de0a9906ebee29fe82087be5ec44f3c01e40730bc808b663c71f19dd6b12f2"
                        },
                        {
                            "emailAddress": "7d2d80e240594dcee2e57f57885ee286c4b232dd142234d7a0af6d31cc86679b"
                        },
                        {
                            "emailAddress": "063c45291827d4994f98357b72862b25885834fd53cf8f0e4af359916fc03c62"
                        },
                        {
                            "phoneNumber": "fb4f73a6ec5fdb7077d564cdd22c3554b43ce49168550c3b12c547b78c517b30"
                        },
                        {
                            "phoneNumber": "61d6c2976b5d92b1ad63073a12fddd26040cd980067d03a93dd1cc50f1a3ac68"
                        },
                        {
                            "phoneNumber": "c5383c2eeada28210d27f011bc127d4f9562cf38ff0c9bba33dd45ae79b9fe7c"
                        }
                    ]
                }
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "05bb62526f091b45d20e243d194766cca8869137421047dc53fa4876d111a6f0"
                        },
                        {
                            "emailAddress": "f1fcde379f31f4d446b76ee8f34860eca2288adc6b6d6c0fdc56d9eee75a2fa5"
                        }
                    ]
                },
                "ipData": [
                    {
                        "ipAddress": "203.0.113.98",
                        "observeStartTime": "2026-06-10T20:17:52.0-04:00",
                        "observeEndTime": "2026-06-17T04:02:04.123-04:00"
                    }
                ]
            }
        },
        {
            "compositeData": {
                "userData": {
                    "userIdentifiers": [
                        {
                            "emailAddress": "83a834cc5327bc4dee7c5408988040dc5813c7662611cd93b707aff72bf7d33f"
                        },
                        {
                            "emailAddress": "223ebda6f6889b1494551ba902d9d381daf2f642bae055888e96343d53e9f9c4"
                        },
                        {
                            "phoneNumber": "525c2390a4e2a9766fa8ddc341dd10414603baebefa401f5a5e29f55e176525d"
                        }
                    ]
                }
            }
        }
    ],
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "encoding": "HEX",
    "termsOfService": {
        "customerMatchTermsOfServiceStatus": "ACCEPTED"
    },
    "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 a <see cref="IngestAudienceMembersRequest" /> without using encryption.
    //
    // User data is read from a data file. See the <c>audience_members_1.json</c> file in the
    // <c>sampledata</c> directory for an example.
    // </summary>
    public class IngestAudienceMembers
    {
        private static readonly int MaxMembersPerRequest = 10_000;

        [Verb(
            "ingest-audience-members",
            HelpText = "Sends an IngestAudienceMembersRequest 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(
                "linkedAccountType",
                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("audienceId", Required = true, HelpText = "ID of the audience")]
            public string AudienceId { 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.AudienceId,
                options.JsonFile,
                options.ValidateOnly
            );
        }

        // Runs the example.
        private void RunExample(
            AccountType operatingAccountType,
            string operatingAccountId,
            AccountType? loginAccountType,
            string? loginAccountId,
            AccountType? linkedAccountType,
            string? linkedAccountId,
            string audienceId,
            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 the audience members from the JSON file.
            List<Member> memberList = ReadMemberData(jsonFile);

            // Creates a factory that will be used to generate the appropriate data manager.
            var userDataFormatter = new UserDataFormatter();

            var audienceMembers = new List<AudienceMember>();

            // Processes each batch of audience members.
            foreach (var member in memberList)
            {
                var compositeData = new CompositeData();
                var userDataBuilder = new UserData();

                // Adds a UserIdentifier for each valid email address for the member.
                foreach (var email in member.Emails)
                {
                    try
                    {
                        string processedEmail = userDataFormatter.ProcessEmailAddress(
                            email,
                            UserDataFormatter.Encoding.Hex
                        );
                        // Sets the email address identifier to the encoded hash.
                        userDataBuilder.UserIdentifiers.Add(
                            new UserIdentifier { EmailAddress = processedEmail }
                        );
                    }
                    catch (ArgumentException)
                    {
                        // Skips invalid input.
                        continue;
                    }
                }

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

                // Adds IP address information for each valid entry for the member.
                var ipDatas = new List<IpData>();
                foreach (var ipInfo in member.IpInfos)
                {
                    if (operatingAccountType != AccountType.GoogleAds)
                    {
                        Console.WriteLine(
                            $"Skipping IP address information for operating account type {operatingAccountType}. "
                                + "Sending IP address is only supported for operating account type GOOGLE_ADS."
                        );
                        continue;
                    }

                    string processedIpAddress = (ipInfo.IpAddress ?? "").Trim();
                    if (string.IsNullOrEmpty(processedIpAddress))
                    {
                        Console.WriteLine("Skipping IP address information with no IP address");
                        continue;
                    }

                    var ipData = new IpData { IpAddress = processedIpAddress };

                    string startTimeString = (ipInfo.ObserveStartTime ?? "").Trim();
                    if (!string.IsNullOrEmpty(startTimeString))
                    {
                        try
                        {
                            ipData.ObserveStartTime = Timestamp.FromDateTime(
                                DateTime.Parse(startTimeString).ToUniversalTime()
                            );
                        }
                        catch (FormatException)
                        {
                            Console.WriteLine(
                                $"Ignoring observe start time '{startTimeString}' since it can't be parsed"
                            );
                        }
                    }

                    string endTimeString = (ipInfo.ObserveEndTime ?? "").Trim();
                    if (!string.IsNullOrEmpty(endTimeString))
                    {
                        try
                        {
                            ipData.ObserveEndTime = Timestamp.FromDateTime(
                                DateTime.Parse(endTimeString).ToUniversalTime()
                            );
                        }
                        catch (FormatException)
                        {
                            Console.WriteLine(
                                $"Ignoring observe end time '{endTimeString}' since it can't be parsed"
                            );
                        }
                    }

                    ipDatas.Add(ipData);
                }

                if (userDataBuilder.UserIdentifiers.Any() || ipDatas.Any())
                {
                    if (userDataBuilder.UserIdentifiers.Any())
                    {
                        compositeData.UserData = userDataBuilder;
                    }
                    if (ipDatas.Any())
                    {
                        compositeData.IpData.AddRange(ipDatas);
                    }
                    audienceMembers.Add(new AudienceMember { CompositeData = compositeData });
                }
            }

            // Builds the Destination for the request.
            var destinationBuilder = new Destination
            {
                // The destination account for the data.
                OperatingAccount = new ProductAccount
                {
                    AccountType = operatingAccountType,
                    AccountId = operatingAccountId,
                },
                // The ID of the user list that is being updated.
                ProductDestinationId = audienceId,
            };

            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 audience members per request.
            for (var i = 0; i < audienceMembers.Count; i += MaxMembersPerRequest)
            {
                IEnumerable<AudienceMember> membersBatch = audienceMembers
                    .Skip(i)
                    .Take(MaxMembersPerRequest);
                requestCount++;
                // Builds the request.
                var request = new IngestAudienceMembersRequest
                {
                    Destinations = { destinationBuilder },
                    // Adds members from the current batch.
                    AudienceMembers = { membersBatch },
                    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,
                    TermsOfService = new TermsOfService
                    {
                        CustomerMatchTermsOfServiceStatus = TermsOfServiceStatus.Accepted,
                    },
                };

                // Logs the request.
                Console.WriteLine($"Request #{requestCount}:\n{request}");

                // Sends the data to the Data Manager API.
                IngestAudienceMembersResponse response =
                    ingestionServiceClient.IngestAudienceMembers(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 Member
        {
            public List<string> Emails { get; set; } = new List<string>();
            public List<string> PhoneNumbers { get; set; } = new List<string>();
            public List<IpInfo> IpInfos { get; set; } = new List<IpInfo>();

            public class IpInfo
            {
                public string? IpAddress { get; set; }
                public string? ObserveStartTime { get; set; }
                public string? ObserveEndTime { get; set; }
            }
        }

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

جاوا

// 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.Encrypter;
import com.google.ads.datamanager.util.UserDataFormatter;
import com.google.ads.datamanager.util.UserDataFormatter.Encoding;
import com.google.ads.datamanager.v1.AudienceMember;
import com.google.ads.datamanager.v1.CompositeData;
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.EncryptionInfo;
import com.google.ads.datamanager.v1.GcpWrappedKeyInfo;
import com.google.ads.datamanager.v1.GcpWrappedKeyInfo.KeyType;
import com.google.ads.datamanager.v1.IngestAudienceMembersRequest;
import com.google.ads.datamanager.v1.IngestAudienceMembersResponse;
import com.google.ads.datamanager.v1.IngestionServiceClient;
import com.google.ads.datamanager.v1.IpData;
import com.google.ads.datamanager.v1.ProductAccount;
import com.google.ads.datamanager.v1.ProductAccount.AccountType;
import com.google.ads.datamanager.v1.TermsOfService;
import com.google.ads.datamanager.v1.TermsOfServiceStatus;
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.security.GeneralSecurityException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Sends an {@link IngestAudienceMembersRequest} with the option to use encryption.
 *
 * <p>User data is read from a data file. See the {@code audience_members_1.json} file in the {@code
 * resources/sampledata} directory for a sample file.
 */
public class IngestAudienceMembers {
  private static final Logger LOGGER = Logger.getLogger(IngestAudienceMembers.class.getName());

  /** The maximum number of audience members allowed per request. */
  private static final int MAX_MEMBERS_PER_REQUEST = 10_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 = "--audienceId", required = true, description = "ID of the audience")
    String audienceId;

    @Parameter(
        names = "--jsonFile",
        required = true,
        description = "JSON file containing user data to ingest")
    String jsonFile;

    @Parameter(
        names = "--keyUri",
        required = false,
        description =
            "URI of the Google Cloud KMS key for encrypting data. If this parameter is set, you"
                + " must also set the --wipProvider parameter.")
    String keyUri;

    @Parameter(
        names = "--wipProvider",
        required = false,
        description =
            "Workload Identity Pool provider name for encrypting data. If this parameter is set,"
                + " you must also set the --keyUri parameter. The argument for this parameter must"
                + " follow the pattern:"
                + " projects/PROJECT_ID/locations/global/workloadIdentityPools/WIP_ID/providers/PROVIDER_ID")
    String wipProvider;

    @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, GeneralSecurityException {
    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");
    }
    if ((paramsConfig.keyUri == null) != (paramsConfig.wipProvider == null)) {
      throw new IllegalArgumentException(
          "Must specify either both or neither of key URI and WIP provider");
    }
    new IngestAudienceMembers().runExample(paramsConfig);
  }

  /**
   * Runs the example.
   *
   * @param params the parameters for the example
   */
  private void runExample(ParamsConfig params) throws IOException, GeneralSecurityException {
    // Reads member data from the data file.
    List<Member> memberList = readMemberData(params.jsonFile);

    // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
    UserDataFormatter userDataFormatter = UserDataFormatter.create();

    // Determines if encryption parameters are set.
    boolean useEncryption = (params.keyUri != null && params.wipProvider != null);
    Encrypter encrypter = null;
    if (useEncryption) {
      // Gets an instance of the encryption utility.
      encrypter = Encrypter.createForGcpKms(params.keyUri, null);
    }
    // Builds the audience_members collection for the request.
    List<AudienceMember> audienceMembers = new ArrayList<>();
    for (Member member : memberList) {
      // Creates a builder for CompositeData to add user data and/or IP address information.
      CompositeData.Builder compositeDataBuilder = CompositeData.newBuilder();

      // Creates a builder for user data for hashed emails and phone numbers.
      UserData.Builder userDataBuilder = compositeDataBuilder.getUserDataBuilder();

      // Adds a UserIdentifier for each valid email address for the member.
      for (String email : member.emails) {
        String processedEmail;
        try {
          processedEmail =
              useEncryption
                  ? userDataFormatter.processEmailAddress(email, Encoding.HEX, encrypter)
                  : userDataFormatter.processEmailAddress(email, Encoding.HEX);
        } catch (IllegalArgumentException iae) {
          // Skips invalid input.
          continue;
        }
        // Sets the email address identifier to the encoded and possibly encrypted email hash.
        userDataBuilder.addUserIdentifiers(
            UserIdentifier.newBuilder().setEmailAddress(processedEmail));
      }

      // Adds a UserIdentifier for each valid phone number for the member.
      for (String phoneNumber : member.phoneNumbers) {
        String processedPhoneNumber;
        try {
          processedPhoneNumber =
              useEncryption
                  ? userDataFormatter.processPhoneNumber(phoneNumber, Encoding.HEX, encrypter)
                  : userDataFormatter.processPhoneNumber(phoneNumber, Encoding.HEX);
        } catch (IllegalArgumentException iae) {
          // Skips invalid input.
          continue;
        }
        // Sets the phone number identifier to the encoded and possibly encrypted phone number
        // hash.
        userDataBuilder.addUserIdentifiers(
            UserIdentifier.newBuilder().setPhoneNumber(processedPhoneNumber));
      }

      // Adds IP address information for each valid entry for the member.
      for (Member.IpInfo ipInfo : member.ipInfos) {
        if (!AccountType.GOOGLE_ADS.equals(params.operatingAccountType)) {
          LOGGER.log(
              Level.INFO,
              "Skipping IP address information for operating account type {0}. "
                  + "Sending IP address is only supported for operating account type {1}.",
              new Object[] {params.operatingAccountType, AccountType.GOOGLE_ADS});
          continue;
        }

        // Trims leading and trailing whitespace from the IP address.
        String processedIpAddress = Strings.nullToEmpty(ipInfo.ipAddress).trim();
        if (processedIpAddress.isEmpty()) {
          // Skips the IP information from the input file since it is missing IP address, which is
          // required.
          LOGGER.info("Skipping IP address information with no IP address");
          continue;
        }

        // Creates an IpData builder and sets its IP address.
        IpData.Builder ipDataBuilder =
            compositeDataBuilder.addIpDataBuilder().setIpAddress(processedIpAddress);

        // Adds observe start time to the IpData if present and in the expected timestamp format.
        String startTimeString = Strings.nullToEmpty(ipInfo.observeStartTime).trim();
        if (!startTimeString.isEmpty()) {
          try {
            ipDataBuilder.setObserveStartTime(Timestamps.parse(startTimeString));
          } catch (ParseException e) {
            LOGGER.log(
                Level.INFO,
                "Ignoring observe start time '{0} since it can't be parsed",
                startTimeString);
          }
        }

        // Adds observe end time to the IpData if present and in the expected timestamp format.
        String endTimeString = Strings.nullToEmpty(ipInfo.observeEndTime).trim();
        if (!endTimeString.isEmpty()) {
          try {
            ipDataBuilder.setObserveEndTime(Timestamps.parse(endTimeString));
          } catch (ParseException e) {
            LOGGER.log(
                Level.INFO,
                "Ignoring observe end time '{0}' since it can't be parsed",
                endTimeString);
          }
        }
      }

      if (userDataBuilder.getUserIdentifiersCount() > 0
          || compositeDataBuilder.getIpDataCount() > 0) {
        // Adds the CompositeData containing user data, IP address information, or both.
        audienceMembers.add(
            AudienceMember.newBuilder().setCompositeData(compositeDataBuilder).build());
      }
    }

    // Builds the Destination for the request.
    Destination.Builder destinationBuilder =
        Destination.newBuilder()
            .setOperatingAccount(
                ProductAccount.newBuilder()
                    .setAccountType(params.operatingAccountType)
                    .setAccountId(params.operatingAccountId))
            .setProductDestinationId(params.audienceId);
    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));
    }

    // Configures the EncryptionInfo for the request if encryption parameters provided.
    EncryptionInfo encryptionInfo = null;
    if (useEncryption) {
      encryptionInfo =
          EncryptionInfo.newBuilder()
              .setGcpWrappedKeyInfo(
                  GcpWrappedKeyInfo.newBuilder()
                      .setKekUri(params.keyUri)
                      .setWipProvider(params.wipProvider)
                      .setKeyType(KeyType.XCHACHA20_POLY1305)
                      // Sets the encrypted_dek field to the Base64-encoded encrypted DEK.
                      .setEncryptedDek(
                          userDataFormatter.base64Encode(
                              encrypter.getEncryptedDek().toByteArray())))
              .build();
    }

    try (IngestionServiceClient ingestionServiceClient = IngestionServiceClient.create()) {
      int requestCount = 0;
      // Batches requests to send up to the maximum number of audience members per request.
      for (List<AudienceMember> audienceMembersBatch :
          Lists.partition(audienceMembers, MAX_MEMBERS_PER_REQUEST)) {
        requestCount++;
        // Builds the request.
        IngestAudienceMembersRequest.Builder requestBuilder =
            IngestAudienceMembersRequest.newBuilder()
                .addDestinations(destinationBuilder)
                // Adds members from the current batch.
                .addAllAudienceMembers(audienceMembersBatch)
                .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)
                .setTermsOfService(
                    TermsOfService.newBuilder()
                        .setCustomerMatchTermsOfServiceStatus(TermsOfServiceStatus.ACCEPTED));

        if (useEncryption) {
          // Sets encryption info on the request.
          requestBuilder.setEncryptionInfo(encryptionInfo);
        }

        // Builds and logs the request.
        IngestAudienceMembersRequest request = requestBuilder.build();
        if (LOGGER.isLoggable(Level.INFO)) {
          LOGGER.info(String.format("Request #%d:%n%s", requestCount, request));
        }

        // Sends the request.
        IngestAudienceMembersResponse response =
            ingestionServiceClient.ingestAudienceMembers(request);

        // Logs the response.
        if (LOGGER.isLoggable(Level.INFO)) {
          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 Member {
    private List<String> emails = new ArrayList<>();
    private List<String> phoneNumbers = new ArrayList<>();
    private List<IpInfo> ipInfos = new ArrayList<>();

    private static class IpInfo {
      private String ipAddress;
      private String observeStartTime;
      private String observeEndTime;
    }
  }

  /**
   * Reads the data file and parses it into a list of {@link IngestAudienceMembers.Member} objects.
   *
   * @param jsonFile the JSON data file
   * @return a list of Member objects
   */
  private List<Member> readMemberData(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 Member objects)
      Type recordListType = new TypeToken<ArrayList<Member>>() {}.getType();

      // Parse the JSON string from the file into a List of Member 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 {
  AudienceMember,
  CompositeData,
  Destination,
  Encoding: DataManagerEncoding,
  Consent,
  ConsentStatus,
  IngestAudienceMembersRequest,
  IpData,
  ProductAccount,
  TermsOfService,
  TermsOfServiceStatus,
  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_MEMBERS_PER_REQUEST = 10000;

interface Arguments {
  operating_account_type: string;
  operating_account_id: string;
  audience_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 MemberRow {
  emails?: string[];
  phoneNumbers?: string[];
  ipInfos?: {
    ipAddress: string;
    observeStartTime?: string;
    observeEndTime?: string;
  }[];
}

/**
 * The main function for the IngestAudienceMembers 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('audience_id', {
      describe: 'The ID of the destination audience.',
      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();

  const formatter = new UserDataFormatter();

  const memberRows: MemberRow[] = await readMemberData(argv.json_file);

  // Converts the operating account type argument to the corresponding enum
  // so IP address checks can compare enums to enums.
  const operatingAccountType = convertToAccountType(
    argv.operating_account_type,
    'operating_account_type',
  );

  // Builds the audience_members collection for the request.
  const audienceMembers = [];
  for (const memberRow of memberRows) {
    const userData = UserData.create();

    // Adds a UserIdentifier for each valid email address for the member.
    for (const email of memberRow.emails || []) {
      try {
        const processedEmail = formatter.processEmailAddress(
          email,
          Encoding.HEX,
        );
        userData.userIdentifiers.push(
          UserIdentifier.create({emailAddress: processedEmail}),
        );
      } catch (e) {
        // Skip invalid input.
      }
    }

    // Adds a UserIdentifier for each valid phone number for the member.
    for (const phone of memberRow.phoneNumbers || []) {
      try {
        const processedPhone = formatter.processPhoneNumber(
          phone,
          Encoding.HEX,
        );
        userData.userIdentifiers.push(
          UserIdentifier.create({phoneNumber: processedPhone}),
        );
      } catch (e) {
        // Skip invalid input.
      }
    }

    // Process IP address information
    const ipDatas = [];
    for (const ipInfo of memberRow.ipInfos || []) {
      if (operatingAccountType !== ProductAccount.AccountType.GOOGLE_ADS) {
        console.log(
          `Skipping IP address information for operating account type ${operatingAccountType}. ` +
            'Sending IP address is only supported for operating account type GOOGLE_ADS.',
        );
        continue;
      }

      const ipAddress = (ipInfo.ipAddress || '').trim();
      if (!ipAddress) {
        console.log('Skipping IP address information with no IP address');
        continue;
      }

      const ipData = IpData.create({ipAddress});

      if (ipInfo.observeStartTime) {
        const startTimeStr = ipInfo.observeStartTime.trim();
        if (startTimeStr) {
          try {
            const date = new Date(startTimeStr);
            if (isNaN(date.getTime())) {
              throw new Error('Invalid date');
            }
            ipData.observeStartTime = {
              seconds: Math.floor(date.getTime() / 1000),
              nanos: (date.getTime() % 1000) * 1e6,
            };
          } catch (e) {
            console.log(
              `Ignoring observe start time '${startTimeStr}' since it can't be parsed`,
            );
          }
        }
      }

      if (ipInfo.observeEndTime) {
        const endTimeStr = ipInfo.observeEndTime.trim();
        if (endTimeStr) {
          try {
            const date = new Date(endTimeStr);
            if (isNaN(date.getTime())) {
              throw new Error('Invalid date');
            }
            ipData.observeEndTime = {
              seconds: Math.floor(date.getTime() / 1000),
              nanos: (date.getTime() % 1000) * 1e6,
            };
          } catch (e) {
            console.log(
              `Ignoring observe end time '${endTimeStr}' since it can't be parsed`,
            );
          }
        }
      }

      ipDatas.push(ipData);
    }

    if (userData.userIdentifiers.length > 0 || ipDatas.length > 0) {
      const compositeData = CompositeData.create();
      if (userData.userIdentifiers.length > 0) {
        compositeData.userData = userData;
      }
      if (ipDatas.length > 0) {
        compositeData.ipData = ipDatas;
      }
      audienceMembers.push(AudienceMember.create({compositeData}));
    } else {
      console.warn('Ignoring line. No data.');
    }
  }

  const destination = Destination.create({
    operatingAccount: ProductAccount.create({
      accountType: operatingAccountType,
      accountId: argv.operating_account_id,
    }),
    productDestinationId: argv.audience_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 audience members per request.
  for (let i = 0; i < audienceMembers.length; i += MAX_MEMBERS_PER_REQUEST) {
    requestCount++;
    const audienceMembersBatch = audienceMembers.slice(
      i,
      i + MAX_MEMBERS_PER_REQUEST,
    );

    const request = IngestAudienceMembersRequest.create({
      destinations: [destination],
      // Adds members from the current batch.
      audienceMembers: audienceMembersBatch,
      consent: Consent.create({
        adUserData: ConsentStatus.CONSENT_GRANTED,
        adPersonalization: ConsentStatus.CONSENT_GRANTED,
      }),
      termsOfService: TermsOfService.create({
        customerMatchTermsOfServiceStatus: TermsOfServiceStatus.ACCEPTED,
      }),
      // Sets encoding to match the encoding used.
      encoding: DataManagerEncoding.HEX,
      // Sets validate_only. If true, then the Data Manager API only validates the request
      // but doesn't apply changes.
      validateOnly: argv.validate_only,
    });

    console.log(`Request #${requestCount}:\n `, request);

    const [response] = await client.ingestAudienceMembers(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 user data from the given JSON file.
 * @param {string} jsonFile The path to the JSON file.
 * @return {Promise<MemberRow[]>} A promise that resolves with an array of user data.
 */
async function readMemberData(jsonFile: string): Promise<MemberRow[]> {
  const data = await fs.promises.readFile(jsonFile, 'utf8');
  return JSON.parse(data) as MemberRow[];
}

/**
 * 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 IngestAudienceMembersRequest without encryption.
 */

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

use Google\Ads\DataManager\V1\AudienceMember;
use Google\Ads\DataManager\V1\Client\IngestionServiceClient;
use Google\Ads\DataManager\V1\CompositeData;
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\IngestAudienceMembersRequest;
use Google\Ads\DataManager\V1\IpData;
use Google\Ads\DataManager\V1\ProductAccount;
use Google\Ads\DataManager\V1\ProductAccount\AccountType;
use Google\Ads\DataManager\V1\TermsOfService;
use Google\Ads\DataManager\V1\TermsOfServiceStatus;
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;

/**
 * Reads the JSON member data file.
 *
 * @param string $jsonFile The member data file. Expected format is a JSON array of
 * objects, where each object can contain "emails" and "phoneNumbers" arrays.
 * @return array A list of associative arrays, each representing a member.
 */
function readMemberData(string $jsonFile): array
{
    $content = file_get_contents($jsonFile);
    if ($content === false) {
        throw new \RuntimeException(sprintf('Could not read JSON file: %s', $jsonFile));
    }
    $data = json_decode($content, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new \RuntimeException(sprintf('Error decoding JSON: %s', json_last_error_msg()));
    }
    return $data;
}

/**
 * Runs the sample.
 *
 * @param int $operatingAccountType The account type of the operating account.
 * @param string $operatingAccountId The ID of the operating account.
 * @param string $audienceId The ID of the destination audience.
 * @param string $jsonFile The JSON file containing member 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 $audienceId,
    string $jsonFile,
    bool $validateOnly,
    ?int $loginAccountType = null,
    ?string $loginAccountId = null,
    ?int $linkedAccountType = null,
    ?string $linkedAccountId = null
): void {
    // Reads member data from the data file.
    $memberRows = readMemberData($jsonFile);

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

    // Builds the audience_members collection for the request.
    $audienceMembers = [];
    foreach ($memberRows as $memberRow) {
        $identifiers = [];
        // Adds a UserIdentifier for each valid email address for the member.
        $emails = $memberRow['emails'] ?? [];
        foreach ($emails as $email) {
            try {
                // Formats, hashes, and encodes the email address.
                $processedEmail = $formatter->processEmailAddress($email, Encoding::Hex);
                // Sets the email address identifier to the encoded email hash.
                $identifiers[] = (new UserIdentifier())->setEmailAddress($processedEmail);
            } catch (\InvalidArgumentException $e) {
                // Skips invalid input.
                error_log(sprintf('Skipping invalid email: %s', $e->getMessage()));
                continue;
            }
        }

        // Adds a UserIdentifier for each valid phone number for the member.
        $phoneNumbers = $memberRow['phoneNumbers'] ?? [];
        foreach ($phoneNumbers as $phone) {
            try {
                // Formats, hashes, and encodes the phone number.
                $processedPhone = $formatter->processPhoneNumber($phone, Encoding::Hex);
                // Sets the phone number identifier to the encoded phone number hash.
                $identifiers[] = (new UserIdentifier())->setPhoneNumber($processedPhone);
            } catch (\InvalidArgumentException $e) {
                // Skips invalid input.
                error_log(sprintf('Skipping invalid phone: %s', $e->getMessage()));
                continue;
            }
        }

        // Adds IP address information for each valid entry for the member.
        $ipDatas = [];
        $ipInfos = $memberRow['ipInfos'] ?? [];
        foreach ($ipInfos as $ipInfo) {
            if ($operatingAccountType !== AccountType::GOOGLE_ADS) {
                error_log(sprintf(
                    'Skipping IP address information for operating account type %s. '
                    . 'Sending IP address is only supported for operating account type GOOGLE_ADS.',
                    AccountType::name($operatingAccountType)
                ));
                continue;
            }

            // Trims leading and trailing whitespace from the IP address.
            $ipAddress = trim($ipInfo['ipAddress'] ?? '');
            if (empty($ipAddress)) {
                // Skips the IP information from the input file since it is missing IP address, which is
                // required.
                error_log('Skipping IP address information with no IP address');
                continue;
            }

            // Creates an IpData message and sets its IP address.
            $ipData = (new IpData())->setIpAddress($ipAddress);

            // Adds observe start time to the IpData if present and in the expected timestamp format.
            $startTimeStr = trim($ipInfo['observeStartTime'] ?? '');
            if (!empty($startTimeStr)) {
                try {
                    $startTime = new Timestamp();
                    $startTime->fromDateTime(new \DateTime($startTimeStr));
                    $ipData->setObserveStartTime($startTime);
                } catch (\Exception $e) {
                    error_log(sprintf("Ignoring observe start time '%s' since it can't be parsed", $startTimeStr));
                }
            }

            // Adds observe end time to the IpData if present and in the expected timestamp format.
            $endTimeStr = trim($ipInfo['observeEndTime'] ?? '');
            if (!empty($endTimeStr)) {
                try {
                    $endTime = new Timestamp();
                    $endTime->fromDateTime(new \DateTime($endTimeStr));
                    $ipData->setObserveEndTime($endTime);
                } catch (\Exception $e) {
                    error_log(sprintf("Ignoring observe end time '%s' since it can't be parsed", $endTimeStr));
                }
            }

            $ipDatas[] = $ipData;
        }

        if (!empty($identifiers) || !empty($ipDatas)) {
            $compositeData = new CompositeData();
            if (!empty($identifiers)) {
                $userData = (new UserData())->setUserIdentifiers($identifiers);
                $compositeData->setUserData($userData);
            }
            if (!empty($ipDatas)) {
                $compositeData->setIpData($ipDatas);
            }

            // Adds an AudienceMember with the CompositeData.
            $audienceMember = (new AudienceMember())->setCompositeData($compositeData);
            $audienceMembers[] = $audienceMember;
        }
    }

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

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

    $destination->setProductDestinationId($audienceId);

    // Builds the request.
    $request = (new IngestAudienceMembersRequest())
        ->setDestinations([$destination])
        ->setAudienceMembers($audienceMembers)
        ->setConsent((new Consent())
            ->setAdUserData(ConsentStatus::CONSENT_GRANTED)
            ->setAdPersonalization(ConsentStatus::CONSENT_GRANTED)
        )
        ->setTermsOfService((new TermsOfService())
            ->setCustomerMatchTermsOfServiceStatus(TermsOfServiceStatus::ACCEPTED)
        )
        // Sets encoding to match the encoding used.
        ->setEncoding(DataManagerEncoding::HEX)
        // Sets validate_only to true to validate but not apply the changes in the request.
        ->setValidateOnly(true);

    // Creates a client for the ingestion service.
    $client = new IngestionServiceClient();
    try {
        // Logs the request.
        echo "Request:\n" . json_encode(json_decode($request->serializeToJsonString()), JSON_PRETTY_PRINT) . "\n";

        // Sends the request.
        $response = $client->ingestAudienceMembers($request);
        echo "Response:\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";
        }
    } 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::',
        'audience_id:',
        'json_file:',
        'validate_only::'
    ]
);

$operatingAccountType = $options['operating_account_type'] ?? null;
$operatingAccountId = $options['operating_account_id'] ?? null;
$audienceId = $options['audience_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($audienceId) || empty($jsonFile)) {
    echo 'Usage: php ingest_audience_members.php ' .
        '--operating_account_type=<account_type> ' .
        '--operating_account_id=<account_id> ' .
        '--audience_id=<audience_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,
    $audienceId,
    $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 IngestAudienceMembersRequest with the option to use 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 Encrypter
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 audience members allowed per request.
_MAX_MEMBERS_PER_REQUEST = 10_000


def main(
    operating_account_type: str,
    operating_account_id: str,
    audience_id: str,
    json_file: str,
    validate_only: bool,
    login_account_type: Optional[str] = None,
    login_account_id: Optional[str] = None,
    linked_account_type: Optional[str] = None,
    linked_account_id: Optional[str] = None,
    key_uri: str = None,
    wip_provider: 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.
     audience_id: the ID of the destination audience.
     json_file: the JSON file containing member 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.
     key_uri: the URI of the Google Cloud KMS key.
     wip_provider: the Workload Identity Pool provider name. Must follow the pattern:
       projects/PROJECT_ID/locations/global/workloadIdentityPools/WIP_ID/providers/PROVIDER_ID
    """

    # Validates parameter pairs.
    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"
        )
    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"
        )
    if bool(key_uri) != bool(wip_provider):
        raise ValueError(
            "Must specify either both or neither of key URI and WIP provider"
        )

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

    # Determines if encryption parameters are set.
    use_encryption: bool = key_uri and wip_provider

    encrypter: Encrypter = None
    if use_encryption:
        # Creates an instance of the encryption utility.
        encrypter = Encrypter.create_for_gcp_kms(key_uri)

    # Reads the input file.
    member_rows: List[Dict[str, Any]] = read_member_data(json_file)
    audience_members: List[datamanager_v1.AudienceMember] = []
    member_row: Dict[str, Any]
    for member_row in member_rows:
        composite_data = datamanager_v1.CompositeData()
        user_data = datamanager_v1.UserData()
        email: str
        for email in member_row.get("emails", []):
            try:
                processed_email: str = formatter.process_email_address(
                    email, Encoding.HEX, encrypter
                )
                user_data.user_identifiers.append(
                    datamanager_v1.UserIdentifier(email_address=processed_email)
                )
            except ValueError:
                # Skips invalid input.
                continue
        phone: str
        for phone in member_row.get("phoneNumbers", []):
            try:
                processed_phone: str = formatter.process_phone_number(
                    phone, Encoding.HEX, encrypter
                )
                user_data.user_identifiers.append(
                    datamanager_v1.UserIdentifier(phone_number=processed_phone)
                )
            except ValueError:
                # Skips invalid input.
                continue

        for ip_info in member_row.get("ipInfos", []):
            if (
                operating_account_type
                != datamanager_v1.ProductAccount.AccountType.GOOGLE_ADS.name
            ):
                _logger.info(
                    "Skipping IP address information for operating account type %s. "
                    "Sending IP address is only supported for operating account type GOOGLE_ADS.",
                    operating_account_type,
                )
                continue

            ip_address = ip_info.get("ipAddress", "").strip()
            if not ip_address:
                _logger.info(
                    "Skipping IP address information with no IP address"
                )
                continue

            ip_data = datamanager_v1.IpData(ip_address=ip_address)

            start_time_str = ip_info.get("observeStartTime", "").strip()
            if start_time_str:
                try:
                    start_time = Timestamp()
                    start_time.FromJsonString(start_time_str)
                    ip_data.observe_start_time = start_time
                except ValueError:
                    _logger.info(
                        "Ignoring observe start time '%s' since it can't be parsed",
                        start_time_str,
                    )

            end_time_str = ip_info.get("observeEndTime", "").strip()
            if end_time_str:
                try:
                    end_time = Timestamp()
                    end_time.FromJsonString(end_time_str)
                    ip_data.observe_end_time = end_time
                except ValueError:
                    _logger.info(
                        "Ignoring observe end time '%s' since it can't be parsed",
                        end_time_str,
                    )

            composite_data.ip_data.append(ip_data)

        if user_data.user_identifiers or composite_data.ip_data:
            if user_data.user_identifiers:
                composite_data.user_data = user_data
            audience_member = datamanager_v1.AudienceMember(
                composite_data=composite_data
            )
            audience_members.append(audience_member)

    # 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
    if login_account_type or 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:
        destination.linked_account.account_type = linked_account_type
        destination.linked_account.account_id = linked_account_id

    destination.product_destination_id = audience_id

    # Configures the EncryptionInfo for the request if encryption parameters provided.
    if use_encryption:
        encryption_info: datamanager_v1.EncryptionInfo = (
            datamanager_v1.EncryptionInfo(
                gcp_wrapped_key_info=datamanager_v1.GcpWrappedKeyInfo(
                    kek_uri=key_uri,
                    wip_provider=wip_provider,
                    key_type=datamanager_v1.GcpWrappedKeyInfo.KeyType.XCHACHA20_POLY1305,
                    # Sets the encrypted_dek field to the Base64-encoded encrypted DEK.
                    encrypted_dek=formatter.base64_encode(
                        encrypter.encrypted_dek_bytes
                    ),
                )
            )
        )

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

    # Batches requests to send up to the maximum number of audience members per
    # request.
    request_count = 0
    for i in range(0, len(audience_members), _MAX_MEMBERS_PER_REQUEST):
        request_count += 1
        audience_members_batch = audience_members[
            i : i + _MAX_MEMBERS_PER_REQUEST
        ]
        # Sends the request.
        request: datamanager_v1.IngestAudienceMembersRequest = (
            datamanager_v1.IngestAudienceMembersRequest(
                destinations=[destination],
                # Adds members from the current batch.
                audience_members=audience_members_batch,
                consent=datamanager_v1.Consent(
                    ad_user_data=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                    ad_personalization=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                ),
                terms_of_service=datamanager_v1.TermsOfService(
                    customer_match_terms_of_service_status=datamanager_v1.TermsOfServiceStatus.ACCEPTED,
                ),
                # 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,
            )
        )

        if use_encryption:
            # Sets encryption info on the request.
            request.encryption_info = encryption_info

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

        # Sends the request.
        response: datamanager_v1.IngestAudienceMembersResponse = (
            client.ingest_audience_members(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_member_data(json_file: str) -> List[Dict[str, Any]]:
    """Reads the JSON member data file.

    Args:
      json_file: the member data file. Expected format is a JSON array of
        objects, where each object can contain "emails" and "phoneNumbers"
        arrays.
    """
    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 audience members from a CSV 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(
        "--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(
        "--audience_id",
        type=str,
        required=True,
        help="The ID of the destination audience.",
    )
    parser.add_argument(
        "--json_file",
        type=str,
        required=True,
        help="JSON file containing user data to ingest.",
    )
    parser.add_argument(
        "--key_uri",
        type=str,
        required=False,
        help="URI of the Google Cloud KMS key for encrypting data. If this parameter is set, you "
        + "must also set the --wip_provider parameter.",
    )
    parser.add_argument(
        "--wip_provider",
        type=str,
        required=False,
        help="Workload Identity Pool provider name for encrypting data. If this parameter is set, "
        + "you must also set the --key_uri parameter. The argument for this parameter must follow "
        + "the pattern: "
        + "projects/PROJECT_ID/locations/global/workloadIdentityPools/WIP_ID/providers/PROVIDER_ID",
    )
    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.audience_id,
        args.json_file,
        args.validate_only == "true",
        args.login_account_type,
        args.login_account_id,
        args.linked_account_type,
        args.linked_account_id,
        args.key_uri,
        args.wip_provider,
    )

پاسخ‌های موفق

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

پاسخ استاندارد

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

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

پاسخ با هشدار

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

{
  "requestId": "126365e1-16d0-4c81-9de9-f362711e250a",
  "fieldWarnings": [
    {
      "field": "audience_members.audience_members[0].composite_data.user_data.user_identifiers[0]",
      "description": "An unknown warning occurred.",
      "reason": "WARNING_REASON_GENERIC"
    }
  ]
}

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": "audience_members.audience_members[0].composite_data.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": "audience_members.audience_members[0].composite_data.user_data",
            "reason": "INVALID_SHA256_FORMAT"
          }
        ]
      }
    ]
  }
}

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

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

برای مثال، اگر برای شناسه فهرست کاربر 11112222 عضو مخاطب دارید و برای شناسه فهرست کاربر 77778888 عضو مخاطب دیگری دارید، هر دو عضو مخاطب را با تنظیم 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": "11112222",
      "reference": "audience_1"
    },
    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_2_TYPE",
        "accountId": "OPERATING_ACCOUNT_2_ID"
      },
      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_2_TYPE",
        "accountId": "LOGIN_ACCOUNT_2_ID"
      },
      "productDestinationId": "77778888",
      "reference": "audience_2"
    }
  ]

شریک داده

  "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": "11112222",
      "reference": "audience_1"
    },
    {
      "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": "77778888",
      "reference": "audience_2"
    }
  ]

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

{
  "compositeData": {
    "userData": {
      "userIdentifiers": [
        {
          "emailAddress": "07e2f1394b0ea80e2adca010ea8318df697001a005ba7452720edda4b0ce57b3"
        },
        {
          "emailAddress": "1df6b43bc68dd38eca94e6a65b4f466ae537b796c81a526918b40ac4a7b906c7"
        }
      ]
    }
  },
  "destinationReferences": [
    "audience_1"
  ]
}

فیلد destination_references فهرست است، بنابراین می‌توانید چندین مقصد برای عضو مخاطب مشخص کنید. اگر destination_references AudienceMember را تنظیم نکنید، Data Manager API عضو مخاطب را به همه مقصدها در درخواست ارسال می‌کند.

مراحل بعدی