Skip to content

Commit 2754a2f

Browse files
committed
feat: add display_url support to create_ad_creative
display_url ("Use display URL") lets advertisers show a vanity/branding domain to users while the real destination stays in link_url. It is carried on asset_feed_spec.link_urls; setting it routes the creative through the asset_feed_spec path. Works for both image and video creatives.
1 parent e4f872b commit 2754a2f

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

‎meta_ads_mcp/core/ads.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,7 @@ async def create_ad_creative(
17251725
image_hashes: Optional[List[str]] = None,
17261726
video_id: Optional[Union[str, int]] = None,
17271727
thumbnail_url: Optional[str] = None,
1728+
display_url: Optional[str] = None,
17281729
optimization_type: Optional[str] = None,
17291730
dynamic_creative_spec: Optional[Dict[str, Any]] = None,
17301731
call_to_action_type: Optional[str] = None,
@@ -1820,6 +1821,12 @@ async def create_ad_creative(
18201821
a few seconds (poll with get_ad_video until video_status
18211822
is "ready") and retry, or pass thumbnail_url explicitly
18221823
(any public image URL works).
1824+
display_url: Optional vanity/branding URL shown to users ("Use display URL"
1825+
in Ads Manager, e.g. "brand.example") while the real destination
1826+
stays in link_url (e.g. "https://example.com/landing"). Requires
1827+
link_url. Works for image and video creatives. Carried on
1828+
asset_feed_spec.link_urls; setting it routes the creative through
1829+
the asset_feed_spec path.
18231830
optimization_type: Optional. Valid values:
18241831
- "DEGREES_OF_FREEDOM": FLEX (Advantage+) creatives where Meta auto-optimizes
18251832
across all asset combinations. At least one multi-variant asset field required.
@@ -2242,6 +2249,9 @@ async def create_ad_creative(
22422249
use_asset_feed = bool(
22432250
headlines or descriptions or messages or image_hashes or videos or images
22442251
or optimization_type or asset_customization_rules
2252+
# display_url is only accepted on asset_feed_spec.link_urls, never in
2253+
# object_story_spec.video_data, so honoring it requires the AFS path.
2254+
or display_url
22452255
)
22462256

22472257
# Track whether `description` was provided but cannot be rendered in the
@@ -2502,7 +2512,12 @@ async def create_ad_creative(
25022512
# Lead-gen flows can omit a website link_url (lead_gen_form_id
25032513
# provides the destination), so guard link_urls on link_url.
25042514
if link_url:
2505-
asset_feed_spec["link_urls"] = [{"website_url": link_url}]
2515+
link_url_entry: Dict[str, Any] = {"website_url": link_url}
2516+
# display_url ("Use display URL") rides on the link_urls entry —
2517+
# the only place Meta accepts it for a creative.
2518+
if display_url:
2519+
link_url_entry["display_url"] = display_url
2520+
asset_feed_spec["link_urls"] = [link_url_entry]
25062521
if optimization_type:
25072522
asset_feed_spec["optimization_type"] = optimization_type
25082523

‎tests/test_video_creatives.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,3 +1683,93 @@ async def test_videos_array_proceeds_when_thumbnail_fetch_fails():
16831683
# No thumbnail_url on the entry — graceful degradation.
16841684
assert "thumbnail_url" not in videos_out[0]
16851685
assert videos_out[0]["video_id"] == "vid_no_thumb"
1686+
1687+
1688+
@pytest.mark.asyncio
1689+
async def test_video_creative_with_display_url_uses_asset_feed_link_urls():
1690+
"""display_url routes a video creative through asset_feed_spec.link_urls.
1691+
1692+
Meta only accepts display_url on asset_feed_spec.link_urls[].display_url, not
1693+
inside object_story_spec.video_data, so setting it forces the asset_feed_spec
1694+
path.
1695+
"""
1696+
1697+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
1698+
patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
1699+
1700+
mock_discover.return_value = {
1701+
"success": True,
1702+
"page_id": "123456789",
1703+
"page_name": "Test Page",
1704+
}
1705+
1706+
mock_api.side_effect = [
1707+
# 1) Auto-fetch video thumbnail (no thumbnail_url provided)
1708+
{"picture": "https://example.com/auto-thumb.jpg"},
1709+
# 2) POST create creative
1710+
{"id": "creative_display_url_1"},
1711+
# 3) GET creative details
1712+
{"id": "creative_display_url_1", "name": "Display URL Creative", "status": "ACTIVE"},
1713+
]
1714+
1715+
result = await create_ad_creative(
1716+
account_id="act_123456",
1717+
video_id="vid_987654",
1718+
name="Display URL Ad",
1719+
link_url="https://example.com/landing",
1720+
display_url="brand.example",
1721+
message="Check out this video",
1722+
headline="Watch Now",
1723+
call_to_action_type="LEARN_MORE",
1724+
access_token="test_token",
1725+
)
1726+
1727+
creative_data = mock_api.call_args_list[1][0][2]
1728+
1729+
# Routed through asset_feed_spec, not the simple video_data path.
1730+
assert "asset_feed_spec" in creative_data
1731+
assert "video_data" not in creative_data.get("object_story_spec", {})
1732+
1733+
link_urls = creative_data["asset_feed_spec"]["link_urls"]
1734+
assert link_urls == [
1735+
{"website_url": "https://example.com/landing", "display_url": "brand.example"}
1736+
]
1737+
1738+
1739+
@pytest.mark.asyncio
1740+
async def test_image_creative_with_display_url_uses_asset_feed_link_urls():
1741+
"""display_url is also supported for image creatives via asset_feed_spec.link_urls."""
1742+
1743+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
1744+
patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
1745+
1746+
mock_discover.return_value = {
1747+
"success": True,
1748+
"page_id": "123456789",
1749+
"page_name": "Test Page",
1750+
}
1751+
1752+
mock_api.side_effect = [
1753+
# 1) POST create creative
1754+
{"id": "creative_display_url_img"},
1755+
# 2) GET creative details
1756+
{"id": "creative_display_url_img", "name": "Display URL Image", "status": "ACTIVE"},
1757+
]
1758+
1759+
result = await create_ad_creative(
1760+
account_id="act_123456",
1761+
image_hash="abc123",
1762+
name="Display URL Image Ad",
1763+
link_url="https://example.com/landing",
1764+
display_url="brand.example",
1765+
message="Body",
1766+
access_token="test_token",
1767+
)
1768+
1769+
creative_data = mock_api.call_args_list[0][0][2]
1770+
assert "asset_feed_spec" in creative_data
1771+
link_urls = creative_data["asset_feed_spec"]["link_urls"]
1772+
assert link_urls == [
1773+
{"website_url": "https://example.com/landing", "display_url": "brand.example"}
1774+
]
1775+
assert creative_data["asset_feed_spec"]["images"] == [{"hash": "abc123"}]

0 commit comments

Comments
 (0)