**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 [email protected]
To unsubscribe from this group, send email to
[email protected]
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 [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/adwords-api/7f5467aa-52ad-452b-ada2-649458148385n%40googlegroups.com.