@@ -2066,18 +2066,51 @@ async def create_ad_creative(
20662066
20672067 # Determine whether to use asset_feed_spec path:
20682068 # - plural parameters (headlines/descriptions/messages/image_hashes), OR
2069- # - optimization_type is set (FLEX creatives always use asset_feed_spec), OR
2070- # - asset_customization_rules requires asset_feed_spec, OR
2071- # - video_id + description: Meta's video_data rejects "description" directly,
2072- # so route through asset_feed_spec which supports descriptions for video ads
2073- # - video_id + instagram_actor_id: Meta requires ad_formats=["SINGLE_VIDEO"] in
2074- # asset_feed_spec alongside instagram_user_id in object_story_spec — error 1443048
2075- # ("object_story_spec ill formed") occurs when asset_feed_spec is absent. Routing
2076- # through asset_feed_spec ensures ad_formats is automatically included.
2069+ # - optimization_type is set to one of the dynamic-creative modes
2070+ # (DEGREES_OF_FREEDOM, PLACEMENT, ASSET_CUSTOMIZATION, LANGUAGE), OR
2071+ # - asset_customization_rules requires asset_feed_spec.
2072+ #
2073+ # NOTE: `optimization_type="REGULAR"` is a Meta-documented asset_feed_spec
2074+ # value meaning "no extra optimization on top of the spec", but callers
2075+ # also use it as a signal that they do NOT want a dynamic creative. With
2076+ # only single-variant inputs (one message, one headline, one video) the
2077+ # caller's intent is the plain `object_story_spec.video_data` shape, so
2078+ # we treat REGULAR as a no-op for routing and let the simple path render
2079+ # the creative. The value is dropped on the wire (we never send
2080+ # asset_feed_spec.optimization_type=REGULAR for a single-video creative).
2081+ #
2082+ # We do NOT route a single video + instagram_actor_id through asset_feed_spec.
2083+ # Per Meta's docs the canonical shape for a video creative with an Instagram
2084+ # identity is `object_story_spec.video_data` + `instagram_user_id` sibling, no
2085+ # asset_feed_spec. Routing every single-video creative through asset_feed_spec
2086+ # silently produces a "dynamic creative" that CTWA campaigns (OUTCOME_SALES /
2087+ # OUTCOME_ENGAGEMENT with destination=WHATSAPP) reject with
2088+ # `error_subcode 1885392` ("O objetivo da campanha nao e aceito pelo criativo
2089+ # dinamico"). The single-video path keeps the creative regular so it serves in
2090+ # CTWA, lead-gen, traffic, and engagement campaigns alike.
2091+ #
2092+ # `description` is also no longer a routing trigger. Meta's `video_data` schema
2093+ # does not carry a `description` field for a single-video creative, so when a
2094+ # caller passes `description` alongside a single video we drop it and surface a
2095+ # warning in the response (the caller's intent — video + IG + WhatsApp CTA — is
2096+ # what matters, not an unrenderable field). To attach descriptions to a video
2097+ # creative, pass `descriptions=[...]` (plural) or `optimization_type` to opt
2098+ # explicitly into the dynamic-creative path.
2099+ #
2100+ # Normalize REGULAR -> None so the rest of the function does not echo it
2101+ # into asset_feed_spec.optimization_type when asset_feed_spec is built for
2102+ # another reason (e.g. plural params).
2103+ if optimization_type == "REGULAR" :
2104+ optimization_type = None
20772105 use_asset_feed = bool (
20782106 headlines or descriptions or messages or image_hashes or videos or images
2079- or optimization_type or asset_customization_rules or (video_id and description )
2080- or (video_id and instagram_actor_id )
2107+ or optimization_type or asset_customization_rules
2108+ )
2109+
2110+ # Track whether `description` was provided but cannot be rendered in the
2111+ # simple video_data path so we can warn the caller after the API call.
2112+ single_video_description_dropped = bool (
2113+ video_id and description and not use_asset_feed
20812114 )
20822115
20832116 # Track if this is a video creative
@@ -2414,16 +2447,27 @@ async def create_ad_creative(
24142447 # Build call_to_action with the destination URL.
24152448 # For video creatives, link_url MUST go in call_to_action.value.link
24162449 # (not as a top-level field in video_data).
2417- cta_value = {}
2418- if link_url :
2419- cta_value ["link" ] = link_url
2420- if lead_gen_form_id :
2421- cta_value ["lead_gen_form_id" ] = lead_gen_form_id
2422- if phone_number :
2423- # CALL_NOW: Meta v24 supports only
2424- # call_to_action.value.link = "tel:+<E.164 number>".
2425- cta_value ["link" ] = f"tel:{ phone_number } "
24262450 cta_type = call_to_action_type or ("LEARN_MORE" if link_url else None )
2451+ cta_value = {}
2452+ if cta_type == "WHATSAPP_MESSAGE" :
2453+ # Click-to-WhatsApp: Meta derives the destination from the
2454+ # Page's linked WhatsApp number, so the CTA carries no value.
2455+ # Passing ANY extra parameter here (callers commonly send a
2456+ # wa.me URL via link_url) makes Meta v24 reject the creative
2457+ # with code 105 / error_subcode 1815630 ("Too many parameters
2458+ # in Call To Action — Please remove parameter 'link' from the
2459+ # value of WHATSAPP_MESSAGE call to action type"). The correct
2460+ # shape is just {"type": "WHATSAPP_MESSAGE"} with no value.
2461+ pass
2462+ else :
2463+ if link_url :
2464+ cta_value ["link" ] = link_url
2465+ if lead_gen_form_id :
2466+ cta_value ["lead_gen_form_id" ] = lead_gen_form_id
2467+ if phone_number :
2468+ # CALL_NOW: Meta v24 supports only
2469+ # call_to_action.value.link = "tel:+<E.164 number>".
2470+ cta_value ["link" ] = f"tel:{ phone_number } "
24272471 if cta_type :
24282472 cta_data = {"type" : cta_type }
24292473 if cta_value :
@@ -2626,6 +2670,16 @@ async def create_ad_creative(
26262670 "is_dynamic_creative=true, or use image_crops on a single "
26272671 "image_hash for per-placement cropping."
26282672 )
2673+ if single_video_description_dropped :
2674+ warnings_ .append (
2675+ "`description` was dropped because Meta's video_data schema does "
2676+ "not carry a description field for a single-video creative. The "
2677+ "creative was created with message + headline only. To attach a "
2678+ "description to a video creative, pass `descriptions=[...]` "
2679+ "(plural) or `optimization_type` — both route through "
2680+ "asset_feed_spec, which is incompatible with CTWA campaigns "
2681+ "(OUTCOME_SALES/OUTCOME_ENGAGEMENT with destination=WHATSAPP)."
2682+ )
26292683 if warnings_ :
26302684 result ["warning" ] = warnings_ [0 ] if len (warnings_ ) == 1 else warnings_
26312685 return json .dumps (result , indent = 2 )
0 commit comments