@@ -276,28 +276,42 @@ async def update_campaign(
276276 campaign_budget_optimization : Optional [bool ] = None ,
277277 objective : Optional [str ] = None , # Add objective if it's updatable
278278 use_adset_level_budgets : Optional [bool ] = None , # Add other updatable fields as needed based on API docs
279+ adset_budgets : Optional [List [Dict [str , Any ]]] = None ,
279280) -> str :
280281 """
281282 Update an existing campaign in a Meta Ads account.
282283
283284 Note: Campaigns do not support start_time for scheduling — set start_time on the ad set instead.
284285
286+ Migrating CBO (Advantage Campaign Budget) → ABO (ad set level budgets):
287+ Pass `adset_budgets` with one entry per ad set in the campaign. Meta atomically
288+ removes the campaign-level budget and assigns budgets at the ad set level in a
289+ single call. This is Meta's documented mechanism — the legacy
290+ `use_adset_level_budgets=true` flag attempts to clear `daily_budget`/`lifetime_budget`
291+ but Meta silently ignores the empty values, so the migration does not persist.
292+
285293 Args:
286294 campaign_id: Meta Ads campaign ID
287295 access_token: Meta API access token (optional - will use cached token if not provided)
288296 name: New campaign name
289297 status: New campaign status (e.g., 'ACTIVE', 'PAUSED')
290298 special_ad_categories: List of special ad categories if applicable
291- daily_budget: New daily budget in account currency (in cents) as a string.
292- Set to empty string "" to remove the daily budget.
293- lifetime_budget: New lifetime budget in account currency (in cents) as a string.
294- Set to empty string "" to remove the lifetime budget.
299+ daily_budget: New daily budget in account currency (in cents).
300+ lifetime_budget: New lifetime budget in account currency (in cents).
295301 bid_strategy: New bid strategy
296302 bid_cap: New bid cap in account currency (in cents) as a string
297303 spend_cap: New spending limit for the campaign in account currency (in cents) as a string
298304 campaign_budget_optimization: Enable/disable campaign budget optimization
299305 objective: New campaign objective (Note: May not always be updatable)
300- use_adset_level_budgets: If True, removes campaign-level budgets to switch to ad set level budgets
306+ use_adset_level_budgets: Deprecated for CBO → ABO migration — use `adset_budgets`
307+ instead. Kept for backwards compatibility; sends empty `daily_budget`/
308+ `lifetime_budget` which Meta silently ignores in most cases.
309+ adset_budgets: List of `{"adset_id": "...", "daily_budget": <cents>}` objects.
310+ Use to migrate from CBO to ABO: Meta removes the campaign-level Advantage
311+ budget and assigns the provided daily budgets at the ad set level in one
312+ atomic call. Example:
313+ [{"adset_id": "1234", "daily_budget": 5000},
314+ {"adset_id": "5678", "daily_budget": 7000}]
301315 """
302316 if not campaign_id :
303317 return json .dumps ({"error" : "No campaign ID provided" }, indent = 2 )
@@ -364,6 +378,12 @@ async def update_campaign(
364378 if objective is not None :
365379 params ["objective" ] = objective # Caution: Objective changes might reset learning or be restricted
366380
381+ # adset_budgets migrates CBO → ABO atomically: Meta removes the campaign-level
382+ # budget and assigns the per-ad-set daily budgets in one call.
383+ # make_api_request JSON-encodes lists for POST form data.
384+ if adset_budgets is not None :
385+ params ["adset_budgets" ] = adset_budgets
386+
367387 if not params :
368388 return json .dumps ({"error" : "No update parameters provided" }, indent = 2 )
369389
@@ -372,10 +392,21 @@ async def update_campaign(
372392 data = await make_api_request (endpoint , access_token , params , method = "POST" )
373393
374394 # Add a note about budget strategy if switching to ad set level budgets
375- if use_adset_level_budgets is not None and use_adset_level_budgets :
395+ if adset_budgets is not None :
376396 data ["budget_strategy" ] = "ad_set_level"
377- data ["note" ] = "Campaign updated to use ad set level budgets. Set budgets when creating ad sets within this campaign."
378-
397+ data ["note" ] = (
398+ "Campaign migrated to ad set level budgets via adset_budgets. "
399+ "The campaign-level Advantage budget has been removed and the provided "
400+ "daily budgets have been assigned to each ad set."
401+ )
402+ elif use_adset_level_budgets is not None and use_adset_level_budgets :
403+ data ["budget_strategy" ] = "ad_set_level"
404+ data ["note" ] = (
405+ "use_adset_level_budgets=true sent empty daily_budget/lifetime_budget "
406+ "values; Meta typically ignores these. To reliably migrate CBO → ABO, "
407+ "call update_campaign with adset_budgets=[{adset_id, daily_budget}, ...]."
408+ )
409+
379410 return json .dumps (data , indent = 2 )
380411 except Exception as e :
381412 error_msg = str (e )
0 commit comments