@@ -2269,18 +2269,51 @@ async def create_ad_creative(
22692269
22702270 # Determine whether to use asset_feed_spec path:
22712271 # - plural parameters (headlines/descriptions/messages/image_hashes), OR
2272- # - optimization_type is set (FLEX creatives always use asset_feed_spec), OR
2273- # - asset_customization_rules requires asset_feed_spec, OR
2274- # - video_id + description: Meta's video_data rejects "description" directly,
2275- # so route through asset_feed_spec which supports descriptions for video ads
2276- # - video_id + instagram_actor_id: Meta requires ad_formats=["SINGLE_VIDEO"] in
2277- # asset_feed_spec alongside instagram_user_id in object_story_spec — error 1443048
2278- # ("object_story_spec ill formed") occurs when asset_feed_spec is absent. Routing
2279- # through asset_feed_spec ensures ad_formats is automatically included.
2272+ # - optimization_type is set to one of the dynamic-creative modes
2273+ # (DEGREES_OF_FREEDOM, PLACEMENT, ASSET_CUSTOMIZATION, LANGUAGE), OR
2274+ # - asset_customization_rules requires asset_feed_spec.
2275+ #
2276+ # NOTE: `optimization_type="REGULAR"` is a Meta-documented asset_feed_spec
2277+ # value meaning "no extra optimization on top of the spec", but callers
2278+ # also use it as a signal that they do NOT want a dynamic creative. With
2279+ # only single-variant inputs (one message, one headline, one video) the
2280+ # caller's intent is the plain `object_story_spec.video_data` shape, so
2281+ # we treat REGULAR as a no-op for routing and let the simple path render
2282+ # the creative. The value is dropped on the wire (we never send
2283+ # asset_feed_spec.optimization_type=REGULAR for a single-video creative).
2284+ #
2285+ # We do NOT route a single video + instagram_actor_id through asset_feed_spec.
2286+ # Per Meta's docs the canonical shape for a video creative with an Instagram
2287+ # identity is `object_story_spec.video_data` + `instagram_user_id` sibling, no
2288+ # asset_feed_spec. Routing every single-video creative through asset_feed_spec
2289+ # silently produces a "dynamic creative" that CTWA campaigns (OUTCOME_SALES /
2290+ # OUTCOME_ENGAGEMENT with destination=WHATSAPP) reject with
2291+ # `error_subcode 1885392` ("O objetivo da campanha nao e aceito pelo criativo
2292+ # dinamico"). The single-video path keeps the creative regular so it serves in
2293+ # CTWA, lead-gen, traffic, and engagement campaigns alike.
2294+ #
2295+ # `description` is also no longer a routing trigger. Meta's `video_data` schema
2296+ # does not carry a `description` field for a single-video creative, so when a
2297+ # caller passes `description` alongside a single video we drop it and surface a
2298+ # warning in the response (the caller's intent — video + IG + WhatsApp CTA — is
2299+ # what matters, not an unrenderable field). To attach descriptions to a video
2300+ # creative, pass `descriptions=[...]` (plural) or `optimization_type` to opt
2301+ # explicitly into the dynamic-creative path.
2302+ #
2303+ # Normalize REGULAR -> None so the rest of the function does not echo it
2304+ # into asset_feed_spec.optimization_type when asset_feed_spec is built for
2305+ # another reason (e.g. plural params).
2306+ if optimization_type == "REGULAR" :
2307+ optimization_type = None
22802308 use_asset_feed = bool (
22812309 headlines or descriptions or messages or image_hashes or videos or images
2282- or optimization_type or asset_customization_rules or (video_id and description )
2283- or (video_id and instagram_actor_id )
2310+ or optimization_type or asset_customization_rules
2311+ )
2312+
2313+ # Track whether `description` was provided but cannot be rendered in the
2314+ # simple video_data path so we can warn the caller after the API call.
2315+ single_video_description_dropped = bool (
2316+ video_id and description and not use_asset_feed
22842317 )
22852318
22862319 # Track if this is a video creative
@@ -2617,16 +2650,27 @@ async def create_ad_creative(
26172650 # Build call_to_action with the destination URL.
26182651 # For video creatives, link_url MUST go in call_to_action.value.link
26192652 # (not as a top-level field in video_data).
2620- cta_value = {}
2621- if link_url :
2622- cta_value ["link" ] = link_url
2623- if lead_gen_form_id :
2624- cta_value ["lead_gen_form_id" ] = lead_gen_form_id
2625- if phone_number :
2626- # CALL_NOW: Meta v24 supports only
2627- # call_to_action.value.link = "tel:+<E.164 number>".
2628- cta_value ["link" ] = f"tel:{ phone_number } "
26292653 cta_type = call_to_action_type or ("LEARN_MORE" if link_url else None )
2654+ cta_value = {}
2655+ if cta_type == "WHATSAPP_MESSAGE" :
2656+ # Click-to-WhatsApp: Meta derives the destination from the
2657+ # Page's linked WhatsApp number, so the CTA carries no value.
2658+ # Passing ANY extra parameter here (callers commonly send a
2659+ # wa.me URL via link_url) makes Meta v24 reject the creative
2660+ # with code 105 / error_subcode 1815630 ("Too many parameters
2661+ # in Call To Action — Please remove parameter 'link' from the
2662+ # value of WHATSAPP_MESSAGE call to action type"). The correct
2663+ # shape is just {"type": "WHATSAPP_MESSAGE"} with no value.
2664+ pass
2665+ else :
2666+ if link_url :
2667+ cta_value ["link" ] = link_url
2668+ if lead_gen_form_id :
2669+ cta_value ["lead_gen_form_id" ] = lead_gen_form_id
2670+ if phone_number :
2671+ # CALL_NOW: Meta v24 supports only
2672+ # call_to_action.value.link = "tel:+<E.164 number>".
2673+ cta_value ["link" ] = f"tel:{ phone_number } "
26302674 if cta_type :
26312675 cta_data = {"type" : cta_type }
26322676 if cta_value :
@@ -2829,6 +2873,16 @@ async def create_ad_creative(
28292873 "is_dynamic_creative=true, or use image_crops on a single "
28302874 "image_hash for per-placement cropping."
28312875 )
2876+ if single_video_description_dropped :
2877+ warnings_ .append (
2878+ "`description` was dropped because Meta's video_data schema does "
2879+ "not carry a description field for a single-video creative. The "
2880+ "creative was created with message + headline only. To attach a "
2881+ "description to a video creative, pass `descriptions=[...]` "
2882+ "(plural) or `optimization_type` — both route through "
2883+ "asset_feed_spec, which is incompatible with CTWA campaigns "
2884+ "(OUTCOME_SALES/OUTCOME_ENGAGEMENT with destination=WHATSAPP)."
2885+ )
28322886 if warnings_ :
28332887 result ["warning" ] = warnings_ [0 ] if len (warnings_ ) == 1 else warnings_
28342888 return json .dumps (result , indent = 2 )
0 commit comments