Skip to content

Commit 32f22d0

Browse files
committed
fix: include link URL in object_story_spec for video FLEX creatives
1 parent e69a4e5 commit 32f22d0

File tree

4 files changed

+42
-18
lines changed

4 files changed

+42
-18
lines changed

meta_ads_mcp/core/ads.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ async def get_creative_details(creative_id: str, access_token: Optional[str] = N
242242
# "(#100) Tried accessing nonexisting field" on simple creatives in API v24.
243243
# We fetch the safe fields first, then try dynamic_creative_spec separately.
244244
params = {
245-
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec,url_tags,link_url"
245+
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec{images,videos,bodies,titles,descriptions,link_urls,ad_formats,call_to_action_types,optimization_type},url_tags,link_url"
246246
}
247247
data = await make_api_request(endpoint, access_token, params)
248248

@@ -1256,22 +1256,36 @@ async def create_ad_creative(
12561256

12571257
creative_data["asset_feed_spec"] = asset_feed_spec
12581258

1259-
# For dynamic/FLEX creatives with asset_feed_spec, object_story_spec needs
1260-
# page_id. For non-video, the link URL is already in asset_feed_spec.link_urls
1261-
# so link_data is NOT added here (Meta rejects link_data without image_hash).
1259+
# For asset_feed_spec creatives, object_story_spec needs page_id
1260+
# plus a link anchor. Meta rejects bare page_id (error 2061015).
12621261
if is_video:
1263-
# video_data does NOT support "link" directly — URL goes in
1264-
# call_to_action.value.link or is handled by asset_feed_spec.link_urls.
1262+
# Video FLEX: use video_data with call_to_action carrying
1263+
# the link URL. This is required for Meta to associate the
1264+
# video and destination URL with the creative.
12651265
video_anchor = {"video_id": video_id}
12661266
if thumbnail_url:
12671267
video_anchor["image_url"] = thumbnail_url
1268+
cta_type = call_to_action_type or "LEARN_MORE"
1269+
cta_value = {}
1270+
if link_url:
1271+
cta_value["link"] = link_url
1272+
if lead_gen_form_id:
1273+
cta_value["lead_gen_form_id"] = lead_gen_form_id
1274+
cta_data = {"type": cta_type}
1275+
if cta_value:
1276+
cta_data["value"] = cta_value
1277+
video_anchor["call_to_action"] = cta_data
12681278
creative_data["object_story_spec"] = {
12691279
"page_id": page_id,
12701280
"video_data": video_anchor
12711281
}
12721282
else:
1283+
# Image FLEX: use link_data with the destination URL.
12731284
creative_data["object_story_spec"] = {
1274-
"page_id": page_id
1285+
"page_id": page_id,
1286+
"link_data": {
1287+
"link": link_url
1288+
}
12751289
}
12761290
else:
12771291
if is_video:
@@ -1391,7 +1405,7 @@ async def create_ad_creative(
13911405
creative_id = data["id"]
13921406
creative_endpoint = f"{creative_id}"
13931407
creative_params = {
1394-
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec,url_tags,link_url"
1408+
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec{images,videos,bodies,titles,descriptions,link_urls,ad_formats,call_to_action_types,optimization_type},url_tags,link_url"
13951409
}
13961410

13971411
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)

tests/test_ad_formats_flexible.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,10 @@ async def test_full_flexible_creative_payload(self):
284284
assert afs["call_to_action_types"] == ["SHOP_NOW"]
285285
assert afs["link_urls"] == [{"website_url": "https://example.com"}]
286286

287-
# object_story_spec needs only page_id; link URL is in asset_feed_spec.link_urls
287+
# object_story_spec needs page_id + link_data with destination URL
288288
assert creative_data["object_story_spec"] == {
289-
"page_id": "987654321"
289+
"page_id": "987654321",
290+
"link_data": {"link": "https://example.com"}
290291
}
291292

292293
async def test_backward_compat_simple_creative_unaffected(self):

tests/test_flex_creatives.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,11 @@ async def test_flex_creative_single_image_uses_asset_feed_spec(self):
238238
assert "asset_feed_spec" in creative_data
239239
assert creative_data["asset_feed_spec"]["images"] == [{"hash": "abc123"}]
240240
assert creative_data["asset_feed_spec"]["optimization_type"] == "DEGREES_OF_FREEDOM"
241-
# object_story_spec needs only page_id; link URL is already in asset_feed_spec.link_urls
241+
# object_story_spec needs page_id + link_data with destination URL
242+
# (Meta rejects object_story_spec without link — error 2061015)
242243
assert creative_data["object_story_spec"] == {
243-
"page_id": "987654321"
244+
"page_id": "987654321",
245+
"link_data": {"link": "https://example.com"}
244246
}
245247

246248
async def test_no_optimization_type_unchanged_behavior(self):

tests/test_video_creatives.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,13 @@ async def test_video_creative_asset_feed_spec_path():
202202
assert len(afs["titles"]) == 2
203203
assert len(afs["bodies"]) == 2
204204

205-
# object_story_spec should use video_data anchor without "link"
205+
# Video FLEX: object_story_spec uses video_data with call_to_action
206206
assert "video_data" in creative_data["object_story_spec"]
207-
assert creative_data["object_story_spec"]["video_data"]["video_id"] == "vid_555666"
208-
assert "link" not in creative_data["object_story_spec"]["video_data"]
207+
vd = creative_data["object_story_spec"]["video_data"]
208+
assert vd["video_id"] == "vid_555666"
209+
assert "link" not in vd, "link must NOT be in video_data directly"
210+
assert vd["call_to_action"]["type"] == "LEARN_MORE"
211+
assert vd["call_to_action"]["value"]["link"] == "https://example.com/"
209212

210213

211214
@pytest.mark.asyncio
@@ -248,9 +251,13 @@ async def test_video_creative_with_dof_optimization():
248251
# Auto-fetched thumbnail should be included in videos array
249252
assert afs["videos"] == [{"video_id": "vid_777888", "thumbnail_url": "https://example.com/auto-thumb.jpg"}]
250253

251-
# video_data anchor should have image_url and not "link"
252-
assert creative_data["object_story_spec"]["video_data"]["image_url"] == "https://example.com/auto-thumb.jpg"
253-
assert "link" not in creative_data["object_story_spec"]["video_data"]
254+
# Video FLEX: video_data anchor with call_to_action
255+
assert "video_data" in creative_data["object_story_spec"]
256+
vd = creative_data["object_story_spec"]["video_data"]
257+
assert vd["image_url"] == "https://example.com/auto-thumb.jpg"
258+
assert "link" not in vd
259+
assert vd["call_to_action"]["type"] == "LEARN_MORE"
260+
assert vd["call_to_action"]["value"]["link"] == "https://example.com/"
254261

255262

256263
@pytest.mark.asyncio

0 commit comments

Comments
 (0)