**Describe the bug:** When calling `AdGroupAdService.mutate_ad_group_ads()` to create a Demand Gen multi-asset ad in a child account under my MCC, the API always returns HTTP 403 Forbidden—even though: - Developer token is **Basic** (not Test) and approved for production - OAuth `refresh_token` has `https://www.googleapis.com/auth/adwords` scope - The MCC → child link appears as `manager=FALSE` & `status=ENABLED` in `customer_client` GAQL
**Steps to Reproduce:** 1. Fetch credentials (developer_token, client_id, client_secret, refresh_token, login_customer_id) from AWS SSM. 2. Initialize client: ```python googleads_client = GoogleAdsClient.load_from_dict({ "developer_token": "<REDACTED_DEV_TOKEN>", "client_id": "<REDACTED_CLIENT_ID>", "client_secret": "<REDACTED_CLIENT_SECRET>", "refresh_token": "<REDACTED_REFRESH_TOKEN>", "login_customer_id": "<MY_MCC_ID>", "use_proto_plus": False, }) 3. Create assets under the child with AssetService.mutate_assets(customer_id="<CHILD_ID>", …) → succeeds. 4. Attempt to create the ad: ``` ag_svc = googleads_client.get_service("AdGroupAdService") ad_op = googleads_client.get_type("AdGroupAdOperation")() # … build ad_op.create.demand_gen_multi_asset_ad … ag_svc.mutate_ad_group_ads(customer_id="<CHILD_ID>", operations=[ad_op]) ``` **Expected behavior:** - A 200 OK response with a new Demand Gen ad resource name, identical to how the library works for other ad types. **Client library version and API version:** Client library version: google-ads 21.0.0 Google Ads API version: v18 <!-- Paste the list of dependencies you're using (i.e. `pip freeze`) --> **Request/Response Logs:** ``` [INFO] Request made: ClientCustomerId: <CHILD_ID>, Method: /google.ads.googleads.v18.services.AssetService/MutateAssets, … IsFault: False [INFO] Request made: ClientCustomerId: <CHILD_ID>, Method: /google.ads.googleads.v18.services.AssetService/MutateAssets, … IsFault: False [ERROR] General exception: HTTP Error 403: Forbidden RequestId (AssetService): c_XgB1RAD1XvX3zE4bEGiw RequestId (AdGroupAdService): VqepG-MSt_US-YrrOE0xHQ ``` **Anything else we should know about your project / environment:** - Running on AWS Lambda (Python 3.12), library installed via Lambda Layer - Verified via GAQL: ``` SELECT customer_client.client_customer, customer_client.manager, customer_client.status FROM customer_client WHERE customer_client.manager = FALSE ``` shows <CHILD_ID> with status = ENABLED. - list_accessible_customers() returns only the MCC, not the child (as expected). ### What I’ve tried so far - Ensured login_customer_id remains the MCC and customer_id is the child. - Confirmed developer token is Basic (not Test) in API Center. - Verified OAuth scopes and user access to the child account. - Queried customer_client to confirm the active link. - Attempted rebuilding the client with both MCC and child as login_customer_id. Could you advise: 1. Whether Demand Gen ad creation via AdGroupAdService is fully supported in v18 of this library? 2. If there is any missing configuration or permission that specifically blocks Demand Gen ad creation? 3. Any additional flags or header settings required for Demand Gen campaigns? my full code: ``` import json import uuid import urllib.request import boto3 from google.ads.googleads.client import GoogleAdsClient from google.ads.googleads.errors import GoogleAdsException from google.ads.googleads.v18.enums.types.asset_type import AssetTypeEnum from google.ads.googleads.v18.enums.types.ad_group_ad_status import AdGroupAdStatusEnum # 1) Load your creds from Parameter Store ssm = boto3.client("ssm") param = ssm.get_parameter( Name="GOOGLE_ADS_API_CREDENTIALS", WithDecryption=True ) creds = json.loads(param["Parameter"]["Value"]) # 2) Initialize a single GoogleAdsClient as your MCC googleads_client = GoogleAdsClient.load_from_dict({ "developer_token": creds["developer_token"], "client_id": creds["client_id"], "client_secret": creds["client_secret"], "refresh_token": creds["refresh_token"], "login_customer_id": creds["login_customer_id"], "use_proto_plus": False, }) def _fetch_bytes(url: str) -> bytes: with urllib.request.urlopen(url) as resp: return resp.read() def _create_text_asset(customer_id: str, text: str) -> str: svc = googleads_client.get_service("AssetService") asset = { "name": f"text-{uuid.uuid4()}", "type_": AssetTypeEnum.AssetType.TEXT, "text_asset": {"text": text} } op = {"create": asset} resp = svc.mutate_assets(customer_id=customer_id, operations=[op]) return resp.results[0].resource_name def _create_image_asset(customer_id: str, url: str) -> str: svc = googleads_client.get_service("AssetService") data = _fetch_bytes(url) asset = { "name": f"img-{uuid.uuid4()}", "type_": AssetTypeEnum.AssetType.IMAGE, "image_asset": {"data": data} } op = {"create": asset} resp = svc.mutate_assets(customer_id=customer_id, operations=[op]) return resp.results[0].resource_name def _create_video_asset(customer_id: str, vid: str) -> str: svc = googleads_client.get_service("AssetService") asset = { "name": f"vid-{uuid.uuid4()}", "type_": AssetTypeEnum.AssetType.YOUTUBE_VIDEO, "youtube_video_asset": {"youtube_video_id": vid} } op = {"create": asset} resp = svc.mutate_assets(customer_id=customer_id, operations=[op]) return resp.results[0].resource_name def lambda_handler(event, context): try: # — parse payload — payload = { "customer_id": "12345679", "ad_group_id": "9876543210123", "ad_type": "Single image ad", "headline": "見出しです", "description": "これは説明文です。", "business_name": "Test Inc.", "marketing_image_urls": [ # exactly 600×314 → aspect 1.91:1 "https://dummyimage.com/600x314/0077cc/ffffff.png&text=600x314" ], "logo_image_urls": [ # exactly 128×128 → aspect 1:1 "https://dummyimage.com/128x128/cc3300/ffffff.png&text=128x128" ], "final_url": "http://lemonmon.site/ab/hRtGSVaxcSbqPwDOA", } child_cid = payload["customer_id"].replace("-", "") ad_group_id = payload["ad_group_id"] ad_type = payload["ad_type"].lower() headline = payload["headline"] description = payload["description"] business_name = payload["business_name"] final_url = payload["final_url"].replace("http://", "https://") m_urls = payload.get("marketing_image_urls", []) l_urls = payload.get("logo_image_urls", []) v_ids = payload.get("video_urls", []) # — 1) create assets under the CHILD account — h_res = _create_text_asset(child_cid, headline) d_res = _create_text_asset(child_cid, description) m_res = [_create_image_asset(child_cid, u) for u in m_urls] l_res = [_create_image_asset(child_cid, u) for u in l_urls] v_res = [_create_video_asset(child_cid, v) for v in v_ids] # — 2) build and send the Demand Gen ad — ag_svc = googleads_client.get_service("AdGroupAdService") ad_op = googleads_client.get_type("AdGroupAdOperation")() ad_msg = ad_op.create ad_msg.ad_group = googleads_client.get_service("AdGroupService") \ .ad_group_path(child_cid, ad_group_id) ad_msg.status = AdGroupAdStatusEnum.AdGroupAdStatus.PAUSED ad_msg.ad.final_urls.append(final_url) # choose the correct Demand Gen subtype: if "video" in ad_type and v_res: dg = ad_msg.ad.demand_gen_video_responsive_ad dg.business_name = business_name dg.headlines.append({"text": headline}) dg.descriptions.append({"text": description}) for logo in l_res: dg.logo_images.append({"asset": logo}) for vid in v_res: dg.videos.append({"asset": vid}) else: dg = ad_msg.ad.demand_gen_multi_asset_ad dg.business_name = business_name dg.headlines.append({"text": headline}) dg.descriptions.append({"text": description}) for img in m_res: dg.marketing_images.append({"asset": img}) for logo in l_res: dg.logo_images.append({"asset": logo}) resp = ag_svc.mutate_ad_group_ads( customer_id=child_cid, operations=[ad_op] ) new_ad = resp.results[0].resource_name return { "statusCode": 200, "body": json.dumps({"status": "OK", "ad": new_ad}), "headers": {"Content-Type": "application/json"} } except GoogleAdsException as api_ex: # Surface the API errors errors = [e.message for e in api_ex.failure.errors] return { "statusCode": 500, "body": json.dumps({"status": "ERROR", "errors": errors}) } except Exception as e: # Fallback return { "statusCode": 500, "body": json.dumps({"status": "ERROR", "message": str(e)}) } ``` -- -- =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~ Also find us on our blog: https://googleadsdeveloper.blogspot.com/ =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~ You received this message because you are subscribed to the Google Groups "AdWords API and Google Ads API Forum" group. To post to this group, send email to adwords-api@googlegroups.com To unsubscribe from this group, send email to adwords-api+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/adwords-api?hl=en --- You received this message because you are subscribed to the Google Groups "Google Ads API and AdWords API Forum" group. To unsubscribe from this group and stop receiving emails from it, send an email to adwords-api+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/adwords-api/7f5467aa-52ad-452b-ada2-649458148385n%40googlegroups.com.