meta-ads-mcp-python 1.0.79__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,666 @@
1
+ """Ad Set-related functionality for Meta Ads API."""
2
+
3
+ import json
4
+ from typing import Optional, Dict, Any, List
5
+ from .api import meta_api_tool, make_api_request, ensure_act_prefix
6
+ from .accounts import get_ad_accounts
7
+ from .server import mcp_server
8
+
9
+
10
+ @mcp_server.tool()
11
+ @meta_api_tool
12
+ async def get_adsets(account_id: str, access_token: Optional[str] = None, limit: int = 10, campaign_id: str = "") -> str:
13
+ """
14
+ Get ad sets for a Meta Ads account with optional filtering by campaign.
15
+
16
+ Args:
17
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
18
+ access_token: Meta API access token (optional - will use cached token if not provided)
19
+ limit: Maximum number of ad sets to return (default: 10)
20
+ campaign_id: Optional campaign ID to filter by
21
+ """
22
+ # Require explicit account_id
23
+ if not account_id:
24
+ return json.dumps({"error": "No account ID specified"}, indent=2)
25
+
26
+ account_id = ensure_act_prefix(account_id)
27
+
28
+ # Change endpoint based on whether campaign_id is provided
29
+ if campaign_id:
30
+ endpoint = f"{campaign_id}/adsets"
31
+ params = {
32
+ "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,bid_constraints,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,is_dynamic_creative,frequency_control_specs{event,interval_days,max_frequency},regional_regulated_categories,regional_regulation_identities",
33
+ "limit": limit
34
+ }
35
+ else:
36
+ # Use account endpoint if no campaign_id is given
37
+ endpoint = f"{account_id}/adsets"
38
+ params = {
39
+ "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,bid_constraints,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,is_dynamic_creative,frequency_control_specs{event,interval_days,max_frequency},regional_regulated_categories,regional_regulation_identities",
40
+ "limit": limit
41
+ }
42
+ # Note: Removed the attempt to add campaign_id to params for the account endpoint case,
43
+ # as it was ineffective and the logic now uses the correct endpoint for campaign filtering.
44
+
45
+ data = await make_api_request(endpoint, access_token, params)
46
+
47
+ return json.dumps(data, indent=2)
48
+
49
+
50
+ @mcp_server.tool()
51
+ @meta_api_tool
52
+ async def get_adset_details(adset_id: str, access_token: Optional[str] = None) -> str:
53
+ """
54
+ Get detailed information about a specific ad set.
55
+
56
+ Args:
57
+ adset_id: Meta Ads ad set ID
58
+ access_token: Meta API access token (optional - will use cached token if not provided)
59
+
60
+ Example:
61
+ To call this function through MCP, pass the adset_id as the first argument:
62
+ {
63
+ "args": "YOUR_ADSET_ID"
64
+ }
65
+ """
66
+ if not adset_id:
67
+ return json.dumps({"error": "No ad set ID provided"}, indent=2)
68
+
69
+ endpoint = f"{adset_id}"
70
+ # Explicitly prioritize frequency_control_specs in the fields request
71
+ params = {
72
+ "fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,bid_constraints,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,dsa_beneficiary,dsa_payor,is_dynamic_creative,regional_regulated_categories,regional_regulation_identities"
73
+ }
74
+
75
+ data = await make_api_request(endpoint, access_token, params)
76
+
77
+ # For debugging - check if frequency_control_specs was returned
78
+ if 'frequency_control_specs' not in data:
79
+ data['_meta'] = {
80
+ 'note': 'No frequency_control_specs field was returned by the API. This means either no frequency caps are set or the API did not include this field in the response.'
81
+ }
82
+
83
+ return json.dumps(data, indent=2)
84
+
85
+
86
+ @meta_api_tool
87
+ async def create_adset(
88
+ account_id: str,
89
+ campaign_id: str,
90
+ name: str,
91
+ optimization_goal: str,
92
+ billing_event: str,
93
+ status: str = "PAUSED",
94
+ daily_budget: Optional[int] = None,
95
+ lifetime_budget: Optional[int] = None,
96
+ targeting: Optional[Dict[str, Any]] = None,
97
+ bid_amount: Optional[int] = None,
98
+ bid_strategy: Optional[str] = None,
99
+ bid_constraints: Optional[Dict[str, Any]] = None,
100
+ start_time: Optional[str] = None,
101
+ end_time: Optional[str] = None,
102
+ dsa_beneficiary: Optional[str] = None,
103
+ dsa_payor: Optional[str] = None,
104
+ promoted_object: Optional[Dict[str, Any]] = None,
105
+ destination_type: Optional[str] = None,
106
+ is_dynamic_creative: Optional[bool] = None,
107
+ frequency_control_specs: Optional[List[Dict[str, Any]]] = None,
108
+ multi_advertiser_ads: Optional[int] = None,
109
+ regional_regulated_categories: Optional[List[str]] = None,
110
+ regional_regulation_identities: Optional[Dict[str, Any]] = None,
111
+ attribution_spec: Optional[List[Dict[str, Any]]] = None,
112
+ access_token: Optional[str] = None
113
+ ) -> str:
114
+ """
115
+ Create a new ad set in a Meta Ads account.
116
+
117
+ Args:
118
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
119
+ campaign_id: Meta Ads campaign ID this ad set belongs to
120
+ name: Ad set name
121
+ optimization_goal: Conversion optimization goal. Valid values depend on the campaign objective and destination_type.
122
+ OUTCOME_ENGAGEMENT + destination_type=WEBSITE: OFFSITE_CONVERSIONS, LANDING_PAGE_VIEWS, LINK_CLICKS, IMPRESSIONS, REACH.
123
+ OUTCOME_ENGAGEMENT + On Post: POST_ENGAGEMENT, IMPRESSIONS, REACH.
124
+ OUTCOME_ENGAGEMENT + On Video: THRUPLAY, TWO_SECOND_CONTINUOUS_VIDEO_VIEWS.
125
+ OUTCOME_ENGAGEMENT + On Event: EVENT_RESPONSES, IMPRESSIONS, POST_ENGAGEMENT, REACH.
126
+ OUTCOME_ENGAGEMENT + On Page: PAGE_LIKES.
127
+ OUTCOME_ENGAGEMENT + Messaging (MESSENGER/WHATSAPP/INSTAGRAM_DIRECT): CONVERSATIONS, LINK_CLICKS.
128
+ OUTCOME_TRAFFIC + WEBSITE: LANDING_PAGE_VIEWS, LINK_CLICKS, IMPRESSIONS, REACH.
129
+ OUTCOME_AWARENESS: REACH, IMPRESSIONS, AD_RECALL_LIFT, THRUPLAY.
130
+ OUTCOME_LEADS: LEAD_GENERATION, QUALITY_LEAD (forms), QUALITY_CALL (calls), OFFSITE_CONVERSIONS, LINK_CLICKS (website).
131
+ OUTCOME_SALES: OFFSITE_CONVERSIONS, VALUE, CONVERSATIONS, LINK_CLICKS, IMPRESSIONS, REACH.
132
+ OUTCOME_APP_PROMOTION: APP_INSTALLS, APP_INSTALLS_AND_OFFSITE_CONVERSIONS, VALUE.
133
+ billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
134
+ status: Initial ad set status (default: PAUSED)
135
+ daily_budget: Daily budget in account currency (in cents) as a string.
136
+ CBO NOTE: Do NOT set this if the parent campaign already has a budget
137
+ (Campaign Budget Optimization / CBO mode). Meta only allows budgets at one
138
+ level: either the campaign OR the ad set, not both. If the campaign has a
139
+ daily_budget or lifetime_budget, omit this field -- the ad set will
140
+ automatically use the campaign budget.
141
+ lifetime_budget: Lifetime budget in account currency (in cents) as a string.
142
+ CBO NOTE: Do NOT set this if the parent campaign already has a budget
143
+ (Campaign Budget Optimization / CBO mode). Omit this field when the
144
+ campaign uses CBO -- the ad set inherits the campaign budget automatically.
145
+ targeting: Targeting specs (age, location, interests, etc).
146
+ targeting_automation.advantage_audience defaults to 0 if not set (Meta API v24+ requirement).
147
+ Set to 1 to enable Advantage+ Audience (requires age_max>=65). Use search_interests for interest IDs.
148
+ bid_amount: Bid amount in account currency (in cents).
149
+ REQUIRED for: LOWEST_COST_WITH_BID_CAP, COST_CAP, TARGET_COST.
150
+ NOT USED by: LOWEST_COST_WITH_MIN_ROAS (uses bid_constraints instead).
151
+ May also be required if the parent campaign's bid strategy requires it.
152
+ bid_strategy: Bid strategy. Valid values:
153
+ - 'LOWEST_COST_WITHOUT_CAP' (recommended) - no bid_amount required
154
+ - 'LOWEST_COST_WITH_BID_CAP' - REQUIRES bid_amount
155
+ - 'COST_CAP' - REQUIRES bid_amount
156
+ - 'LOWEST_COST_WITH_MIN_ROAS' - REQUIRES bid_constraints with roas_average_floor,
157
+ and optimization_goal='VALUE'. Does NOT use bid_amount.
158
+ Note: 'LOWEST_COST' is NOT valid - use 'LOWEST_COST_WITHOUT_CAP'.
159
+ Campaign-level bid strategy may constrain ad set choices.
160
+ bid_constraints: Bid constraints dict. Required for LOWEST_COST_WITH_MIN_ROAS.
161
+ Use {"roas_average_floor": <value>} where value = target ROAS * 10000.
162
+ Example: 2.0x ROAS -> {"roas_average_floor": 20000}
163
+ start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800').
164
+ To schedule future delivery: set start_time to a future date and status=ACTIVE.
165
+ Meta will show effective_status as SCHEDULED and automatically begin delivery at start_time.
166
+ NOTE: Only ad set start_time controls delivery scheduling. Campaigns do not support start_time.
167
+ end_time: End time in ISO 8601 format. Required when lifetime_budget is specified.
168
+ dsa_beneficiary: DSA beneficiary for European compliance (person/org that benefits from ads).
169
+ Required for EU-targeted ad sets along with dsa_payor.
170
+ dsa_payor: DSA payor for European compliance (person/org paying for the ads).
171
+ Required for EU-targeted ad sets along with dsa_beneficiary.
172
+ promoted_object: App config for APP_INSTALLS. Required: application_id, object_store_url.
173
+ destination_type: Where users go after click. Common values: 'WEBSITE', 'WHATSAPP', 'MESSENGER',
174
+ 'INSTAGRAM_DIRECT', 'ON_AD', 'APP', 'FACEBOOK', 'SHOP_AUTOMATIC'.
175
+ Also supports multi-channel combos like 'MESSAGING_MESSENGER_WHATSAPP'.
176
+ is_dynamic_creative: Enable Dynamic Creative for this ad set.
177
+ frequency_control_specs: Frequency cap specs. MUST be set at creation time -- Meta makes this field
178
+ immutable after the ad set is created (error 1815198).
179
+ Only works with OUTCOME_AWARENESS campaigns + optimization_goal REACH or THRUPLAY.
180
+ Example: [{"event": "IMPRESSIONS", "interval_days": 7, "max_frequency": 1}]
181
+ multi_advertiser_ads: Set to 0 to opt out of Multi-Advertiser Ads, 1 to opt in.
182
+ This is a TOP-LEVEL ad set parameter -- do NOT put it inside the targeting object.
183
+ regional_regulated_categories: List of regional regulated categories for the ad set.
184
+ Required for ads targeting regulated regions (Taiwan, Australia, etc.).
185
+ Valid values: TAIWAN_FINSERV, TAIWAN_UNIVERSAL, AUSTRALIA_FINSERV,
186
+ INDIA_FINSERV, SINGAPORE_UNIVERSAL, THAILAND_UNIVERSAL.
187
+ Example: ["TAIWAN_UNIVERSAL"] or ["TAIWAN_FINSERV", "TAIWAN_UNIVERSAL"]
188
+ regional_regulation_identities: Dict of verified identity IDs for regional transparency compliance.
189
+ Required when regional_regulated_categories is set.
190
+ The identity IDs come from completing advertiser verification in Meta Business Settings.
191
+ Keys depend on the categories declared:
192
+ - TAIWAN_UNIVERSAL: taiwan_universal_beneficiary, taiwan_universal_payer
193
+ - TAIWAN_FINSERV: taiwan_finserv_beneficiary, taiwan_finserv_payer
194
+ - AUSTRALIA_FINSERV: australia_finserv_beneficiary, australia_finserv_payer
195
+ - SINGAPORE_UNIVERSAL: singapore_universal_beneficiary, singapore_universal_payer
196
+ Example: {"taiwan_universal_beneficiary": "<id>", "taiwan_universal_payer": "<id>"}
197
+ attribution_spec: Attribution window specification for the ad set. Controls how conversions are
198
+ attributed to ads. Default is 7-day click if not specified.
199
+ Example for 1-day click: [{"event_type": "CLICK_THROUGH", "window_days": 1}]
200
+ Example for 1-day click + 1-day view: [{"event_type": "CLICK_THROUGH", "window_days": 1}, {"event_type": "VIEW_THROUGH", "window_days": 1}]
201
+ Valid event_type values: CLICK_THROUGH, VIEW_THROUGH.
202
+ Valid window_days values: 1, 7, 28 (depends on event_type and optimization_goal).
203
+ access_token: Meta API access token (optional - will use cached token if not provided)
204
+ """
205
+ # Check required parameters
206
+ if not account_id:
207
+ return json.dumps({"error": "No account ID provided"}, indent=2)
208
+
209
+ account_id = ensure_act_prefix(account_id)
210
+
211
+ if not campaign_id:
212
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
213
+
214
+ if not name:
215
+ return json.dumps({"error": "No ad set name provided"}, indent=2)
216
+
217
+ if not optimization_goal:
218
+ return json.dumps({"error": "No optimization goal provided"}, indent=2)
219
+
220
+ if not billing_event:
221
+ return json.dumps({"error": "No billing event provided"}, indent=2)
222
+
223
+ # Validate mobile app parameters for APP_INSTALLS campaigns
224
+ if optimization_goal == "APP_INSTALLS":
225
+ if not promoted_object:
226
+ return json.dumps({
227
+ "error": "promoted_object is required for APP_INSTALLS optimization goal",
228
+ "details": "Mobile app campaigns must specify which app is being promoted",
229
+ "required_fields": ["application_id", "object_store_url"]
230
+ }, indent=2)
231
+
232
+ # Validate promoted_object structure
233
+ if not isinstance(promoted_object, dict):
234
+ return json.dumps({
235
+ "error": "promoted_object must be a dictionary",
236
+ "example": {"application_id": "123456789012345", "object_store_url": "https://apps.apple.com/app/id123456789"}
237
+ }, indent=2)
238
+
239
+ # Validate required promoted_object fields
240
+ if "application_id" not in promoted_object:
241
+ return json.dumps({
242
+ "error": "promoted_object missing required field: application_id",
243
+ "details": "application_id is the Facebook app ID for your mobile app"
244
+ }, indent=2)
245
+
246
+ if "object_store_url" not in promoted_object:
247
+ return json.dumps({
248
+ "error": "promoted_object missing required field: object_store_url",
249
+ "details": "object_store_url should be the App Store or Google Play URL for your app"
250
+ }, indent=2)
251
+
252
+ # Validate store URL format
253
+ store_url = promoted_object["object_store_url"]
254
+ valid_store_patterns = [
255
+ "apps.apple.com", # iOS App Store
256
+ "play.google.com", # Google Play Store
257
+ "itunes.apple.com" # Alternative iOS format
258
+ ]
259
+
260
+ if not any(pattern in store_url for pattern in valid_store_patterns):
261
+ return json.dumps({
262
+ "error": "Invalid object_store_url format",
263
+ "details": "URL must be from App Store (apps.apple.com) or Google Play (play.google.com)",
264
+ "provided_url": store_url
265
+ }, indent=2)
266
+
267
+ # destination_type is passed through to Meta's API without client-side validation.
268
+ # Meta supports 23+ values (WHATSAPP, MESSENGER, INSTAGRAM_DIRECT, ON_AD, WEBSITE,
269
+ # APP, FACEBOOK, SHOP_AUTOMATIC, multi-channel MESSAGING_* combos, etc.)
270
+ # and may add more. Let Meta's API reject invalid values.
271
+ # See: facebook-python-business-sdk AdSet.DestinationType
272
+
273
+ # Basic targeting is required if not provided
274
+ if not targeting:
275
+ targeting = {
276
+ "age_min": 18,
277
+ "age_max": 65,
278
+ "geo_locations": {"countries": ["US"]},
279
+ "targeting_automation": {"advantage_audience": 1}
280
+ }
281
+
282
+ # Meta API v24+ requires targeting_automation.advantage_audience.
283
+ # Default to 0 (disabled) when user provides custom targeting, since
284
+ # advantage_audience=1 enforces constraints (e.g. age_max >= 65) that
285
+ # conflict with explicit targeting parameters.
286
+ if "targeting_automation" not in targeting:
287
+ targeting["targeting_automation"] = {"advantage_audience": 0}
288
+
289
+ # Bid strategies that require bid_amount (not bid_constraints)
290
+ strategies_requiring_bid_amount = [
291
+ 'LOWEST_COST_WITH_BID_CAP',
292
+ 'COST_CAP',
293
+ 'TARGET_COST',
294
+ ]
295
+
296
+ # Validate bid_strategy and bid_amount requirements
297
+ if bid_strategy:
298
+ # Check for invalid 'LOWEST_COST' value (common mistake)
299
+ if bid_strategy == 'LOWEST_COST':
300
+ return json.dumps({
301
+ "error": "'LOWEST_COST' is not a valid bid_strategy value",
302
+ "details": "The 'LOWEST_COST' bid strategy is not valid in Meta Ads API v24.0",
303
+ "workaround": "Use 'LOWEST_COST_WITHOUT_CAP' instead (no bid_amount required)",
304
+ "valid_values": [
305
+ "LOWEST_COST_WITHOUT_CAP (recommended - no bid_amount required)",
306
+ "LOWEST_COST_WITH_BID_CAP (requires bid_amount)",
307
+ "COST_CAP (requires bid_amount)",
308
+ "LOWEST_COST_WITH_MIN_ROAS (requires bid_constraints with roas_average_floor)"
309
+ ],
310
+ "example": '{"bid_strategy": "LOWEST_COST_WITHOUT_CAP"}'
311
+ }, indent=2)
312
+
313
+ if bid_strategy in strategies_requiring_bid_amount and bid_amount is None:
314
+ return json.dumps({
315
+ "error": f"bid_amount is required when using bid_strategy '{bid_strategy}'",
316
+ "details": f"The '{bid_strategy}' bid strategy requires you to specify a bid amount in cents",
317
+ "workaround": "Either provide the bid_amount parameter, or use bid_strategy='LOWEST_COST_WITHOUT_CAP' which does not require a bid amount",
318
+ "example_with_bid_amount": f'{{"bid_strategy": "{bid_strategy}", "bid_amount": 500}}',
319
+ "example_without_bid_amount": '{"bid_strategy": "LOWEST_COST_WITHOUT_CAP"}'
320
+ }, indent=2)
321
+
322
+ # LOWEST_COST_WITH_MIN_ROAS requires bid_constraints with roas_average_floor
323
+ if bid_strategy == 'LOWEST_COST_WITH_MIN_ROAS' and not bid_constraints:
324
+ return json.dumps({
325
+ "error": "bid_constraints is required when using bid_strategy 'LOWEST_COST_WITH_MIN_ROAS'",
326
+ "details": "Provide bid_constraints with roas_average_floor (target ROAS * 10000)",
327
+ "example": '{"bid_strategy": "LOWEST_COST_WITH_MIN_ROAS", "bid_constraints": {"roas_average_floor": 20000}, "optimization_goal": "VALUE"}'
328
+ }, indent=2)
329
+
330
+ # Pre-flight check: fetch campaign data to catch common errors before hitting Meta's API.
331
+ # Triggered when the user provides a budget (CBO conflict check) or omits bid_amount
332
+ # (bid strategy compatibility check). A single API call covers both checks.
333
+ needs_campaign_check = (daily_budget is not None or lifetime_budget is not None or bid_amount is None)
334
+ if needs_campaign_check:
335
+ try:
336
+ campaign_data = await make_api_request(
337
+ campaign_id, access_token, {"fields": "bid_strategy,name,daily_budget,lifetime_budget"}
338
+ )
339
+ campaign_name = campaign_data.get("name", campaign_id)
340
+
341
+ # Check 1: CBO budget conflict.
342
+ # Meta does not allow budgets at both the campaign and ad set level.
343
+ # If the campaign already has a budget (CBO mode), reject ad-set-level budgets early.
344
+ if daily_budget is not None or lifetime_budget is not None:
345
+ campaign_daily_budget = campaign_data.get("daily_budget")
346
+ campaign_lifetime_budget = campaign_data.get("lifetime_budget")
347
+ if campaign_daily_budget or campaign_lifetime_budget:
348
+ budget_type = "daily_budget" if campaign_daily_budget else "lifetime_budget"
349
+ return json.dumps({
350
+ "error": f"Budget conflict: campaign '{campaign_name}' ({campaign_id}) already has a {budget_type} set (Campaign Budget Optimization / CBO).",
351
+ "details": "Meta does not allow budgets at both the campaign and ad set level. When a campaign uses CBO, its ad sets must not specify daily_budget or lifetime_budget.",
352
+ "fix": "Remove daily_budget and lifetime_budget from your create_adset call. The ad set will automatically use the campaign budget.",
353
+ "alternative": "To use ad set-level budgets (ABO), you would need to create a campaign without a campaign-level budget."
354
+ }, indent=2)
355
+
356
+ # Check 2: Campaign bid strategy requires bid_amount.
357
+ # This prevents a confusing error from Meta's API when the campaign-level
358
+ # bid strategy forces child ad sets to provide bid_amount.
359
+ if bid_amount is None:
360
+ campaign_bid_strategy = campaign_data.get("bid_strategy")
361
+ if campaign_bid_strategy and campaign_bid_strategy in strategies_requiring_bid_amount:
362
+ return json.dumps({
363
+ "error": f"bid_amount is required because the parent campaign uses bid_strategy '{campaign_bid_strategy}'",
364
+ "details": f"Campaign '{campaign_name}' ({campaign_id}) uses '{campaign_bid_strategy}', which requires all child ad sets to provide a bid_amount (in cents).",
365
+ "workaround": "Either provide the bid_amount parameter, or change the campaign's bid_strategy to 'LOWEST_COST_WITHOUT_CAP'",
366
+ "example_with_bid_amount": f'{{"bid_amount": 500}} (= $5.00 bid cap)',
367
+ "example_without_bid_amount": 'Change campaign bid strategy: update_campaign(campaign_id="' + campaign_id + '", bid_strategy="LOWEST_COST_WITHOUT_CAP")'
368
+ }, indent=2)
369
+ except Exception:
370
+ pass # If the pre-flight check fails, let the create request proceed normally
371
+
372
+ endpoint = f"{account_id}/adsets"
373
+
374
+ params = {
375
+ "name": name,
376
+ "campaign_id": campaign_id,
377
+ "status": status,
378
+ "optimization_goal": optimization_goal,
379
+ "billing_event": billing_event,
380
+ "targeting": json.dumps(targeting) # Properly format as JSON string
381
+ }
382
+
383
+ # Convert budget values to strings if they aren't already
384
+ if daily_budget is not None:
385
+ params["daily_budget"] = str(daily_budget)
386
+
387
+ if lifetime_budget is not None:
388
+ params["lifetime_budget"] = str(lifetime_budget)
389
+
390
+ # Add other parameters if provided
391
+ if bid_amount is not None:
392
+ params["bid_amount"] = str(bid_amount)
393
+
394
+ if bid_strategy:
395
+ params["bid_strategy"] = bid_strategy
396
+
397
+ if bid_constraints:
398
+ params["bid_constraints"] = json.dumps(bid_constraints)
399
+
400
+ if start_time:
401
+ params["start_time"] = start_time
402
+
403
+ if end_time:
404
+ params["end_time"] = end_time
405
+
406
+ # Add DSA fields if provided (both required for EU-targeted ad sets)
407
+ if dsa_beneficiary:
408
+ params["dsa_beneficiary"] = dsa_beneficiary
409
+ if dsa_payor:
410
+ params["dsa_payor"] = dsa_payor
411
+
412
+ # Add mobile app parameters if provided
413
+ if promoted_object:
414
+ params["promoted_object"] = json.dumps(promoted_object)
415
+
416
+ if destination_type:
417
+ params["destination_type"] = destination_type
418
+
419
+ # Enable Dynamic Creative if requested
420
+ if is_dynamic_creative is not None:
421
+ params["is_dynamic_creative"] = "true" if bool(is_dynamic_creative) else "false"
422
+
423
+ if frequency_control_specs is not None:
424
+ params["frequency_control_specs"] = json.dumps(frequency_control_specs)
425
+
426
+ if multi_advertiser_ads is not None:
427
+ params["multi_advertiser_ads"] = str(multi_advertiser_ads)
428
+
429
+ if regional_regulated_categories is not None:
430
+ params["regional_regulated_categories"] = json.dumps(regional_regulated_categories)
431
+
432
+ if regional_regulation_identities is not None:
433
+ params["regional_regulation_identities"] = json.dumps(regional_regulation_identities)
434
+
435
+ if attribution_spec is not None:
436
+ params["attribution_spec"] = json.dumps(attribution_spec)
437
+
438
+ try:
439
+ data = await make_api_request(endpoint, access_token, params, method="POST")
440
+ return json.dumps(data, indent=2)
441
+ except Exception as e:
442
+ error_msg = str(e)
443
+
444
+ # Enhanced error handling for DSA beneficiary issues
445
+ if "permission" in error_msg.lower() or "insufficient" in error_msg.lower():
446
+ return json.dumps({
447
+ "error": "Insufficient permissions to set DSA beneficiary. Please ensure you have business_management permissions.",
448
+ "details": error_msg,
449
+ "params_sent": params,
450
+ "permission_required": True
451
+ }, indent=2)
452
+ elif "dsa_beneficiary" in error_msg.lower() and ("not supported" in error_msg.lower() or "parameter" in error_msg.lower()):
453
+ return json.dumps({
454
+ "error": "DSA beneficiary parameter not supported in this API version. Please set DSA beneficiary manually in Facebook Ads Manager.",
455
+ "details": error_msg,
456
+ "params_sent": params,
457
+ "manual_setup_required": True
458
+ }, indent=2)
459
+ elif "benefits from ads" in error_msg or "DSA beneficiary" in error_msg:
460
+ return json.dumps({
461
+ "error": "DSA beneficiary required for European compliance. Please provide the person or organization that benefits from ads in this ad set.",
462
+ "details": error_msg,
463
+ "params_sent": params,
464
+ "dsa_required": True
465
+ }, indent=2)
466
+ else:
467
+ return json.dumps({
468
+ "error": "Failed to create ad set",
469
+ "details": error_msg,
470
+ "params_sent": params
471
+ }, indent=2)
472
+
473
+
474
+ @meta_api_tool
475
+ async def update_adset(adset_id: str, frequency_control_specs: Optional[List[Dict[str, Any]]] = None, bid_strategy: Optional[str] = None,
476
+ bid_amount: Optional[int] = None, bid_constraints: Optional[Dict[str, Any]] = None,
477
+ name: Optional[str] = None,
478
+ status: Optional[str] = None, targeting: Optional[Dict[str, Any]] = None,
479
+ optimization_goal: Optional[str] = None, daily_budget: Optional[int] = None, lifetime_budget: Optional[int] = None,
480
+ is_dynamic_creative: Optional[bool] = None,
481
+ start_time: Optional[str] = None,
482
+ end_time: Optional[str] = None,
483
+ dsa_beneficiary: Optional[str] = None,
484
+ dsa_payor: Optional[str] = None,
485
+ multi_advertiser_ads: Optional[int] = None,
486
+ regional_regulated_categories: Optional[List[str]] = None,
487
+ regional_regulation_identities: Optional[Dict[str, Any]] = None,
488
+ attribution_spec: Optional[List[Dict[str, Any]]] = None,
489
+ access_token: Optional[str] = None) -> str:
490
+ """
491
+ Update an ad set with new settings including frequency caps and budgets.
492
+
493
+ Args:
494
+ adset_id: Meta Ads ad set ID
495
+ name: New ad set name
496
+ frequency_control_specs: Frequency control specs
497
+ (e.g. [{"event": "IMPRESSIONS", "interval_days": 7, "max_frequency": 3}])
498
+ bid_strategy: Bid strategy. Valid values:
499
+ - 'LOWEST_COST_WITHOUT_CAP' (recommended) - no bid_amount required
500
+ - 'LOWEST_COST_WITH_BID_CAP' - REQUIRES bid_amount
501
+ - 'COST_CAP' - REQUIRES bid_amount
502
+ - 'LOWEST_COST_WITH_MIN_ROAS' - REQUIRES bid_constraints with roas_average_floor
503
+ Note: 'LOWEST_COST' is NOT valid - use 'LOWEST_COST_WITHOUT_CAP'.
504
+ bid_amount: Bid amount in cents. Required for LOWEST_COST_WITH_BID_CAP, COST_CAP, TARGET_COST.
505
+ NOT USED by LOWEST_COST_WITH_MIN_ROAS (uses bid_constraints instead).
506
+ bid_constraints: Bid constraints dict. Required for LOWEST_COST_WITH_MIN_ROAS.
507
+ Use {"roas_average_floor": <value>} where value = target ROAS * 10000.
508
+ Example: 2.0x ROAS -> {"roas_average_floor": 20000}
509
+ status: Update ad set status (ACTIVE, PAUSED, etc.)
510
+ targeting: Complete targeting specifications (replaces existing targeting)
511
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'CONVERSIONS', 'VALUE')
512
+ daily_budget: Daily budget in account currency (in cents)
513
+ lifetime_budget: Lifetime budget in account currency (in cents)
514
+ is_dynamic_creative: Enable/disable Dynamic Creative for this ad set.
515
+ WARNING: This field is immutable after ad set creation. Meta's API will
516
+ return success but silently ignore the change. To change this, create a new ad set.
517
+ start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800').
518
+ Use with status=ACTIVE to schedule the ad set for future delivery (effective_status will be SCHEDULED until start_time).
519
+ end_time: End time in ISO 8601 format. Required when lifetime_budget is specified.
520
+ dsa_beneficiary: DSA beneficiary for European compliance (person/org that benefits from ads).
521
+ Required for EU-targeted ad sets along with dsa_payor.
522
+ dsa_payor: DSA payor for European compliance (person/org paying for the ads).
523
+ Required for EU-targeted ad sets along with dsa_beneficiary.
524
+ multi_advertiser_ads: Set to 0 to opt out of Multi-Advertiser Ads, 1 to opt in.
525
+ This is a TOP-LEVEL ad set parameter -- do NOT put it inside the targeting object.
526
+ regional_regulated_categories: List of regional regulated categories for the ad set.
527
+ Required for ads targeting regulated regions (Taiwan, Australia, etc.).
528
+ Valid values: TAIWAN_FINSERV, TAIWAN_UNIVERSAL, AUSTRALIA_FINSERV,
529
+ INDIA_FINSERV, SINGAPORE_UNIVERSAL, THAILAND_UNIVERSAL.
530
+ Set to null/empty to remove existing categories.
531
+ regional_regulation_identities: Dict of verified identity IDs for regional transparency compliance.
532
+ Required when regional_regulated_categories is set.
533
+ Set individual keys to null to remove them.
534
+ attribution_spec: Attribution window specification for the ad set.
535
+ WARNING: Meta no longer supports updating attribution_spec after ad set creation
536
+ (error 1504040). To change attribution windows, create a new ad set instead.
537
+ This parameter is kept for compatibility but will be rejected by Meta's API.
538
+ Valid event_type values: CLICK_THROUGH, VIEW_THROUGH.
539
+ Valid window_days values: 1, 7, 28 (depends on event_type and optimization_goal).
540
+ access_token: Meta API access token (optional - will use cached token if not provided)
541
+ """
542
+ if not adset_id:
543
+ return json.dumps({"error": "No ad set ID provided"}, indent=2)
544
+
545
+ # Validate bid_strategy if provided
546
+ if bid_strategy is not None:
547
+ # Check for invalid 'LOWEST_COST' value (common mistake)
548
+ if bid_strategy == 'LOWEST_COST':
549
+ return json.dumps({
550
+ "error": "'LOWEST_COST' is not a valid bid_strategy value",
551
+ "details": "The 'LOWEST_COST' bid strategy is not valid in Meta Ads API v24.0",
552
+ "workaround": "Use 'LOWEST_COST_WITHOUT_CAP' instead (no bid_amount required)",
553
+ "valid_values": [
554
+ "LOWEST_COST_WITHOUT_CAP (recommended - no bid_amount required)",
555
+ "LOWEST_COST_WITH_BID_CAP (requires bid_amount)",
556
+ "COST_CAP (requires bid_amount)",
557
+ "LOWEST_COST_WITH_MIN_ROAS (requires bid_constraints with roas_average_floor)"
558
+ ],
559
+ "example": '{"bid_strategy": "LOWEST_COST_WITHOUT_CAP"}'
560
+ }, indent=2)
561
+
562
+ # Bid strategies that require bid_amount (not bid_constraints)
563
+ strategies_requiring_bid_amount = [
564
+ 'LOWEST_COST_WITH_BID_CAP',
565
+ 'COST_CAP',
566
+ 'TARGET_COST',
567
+ ]
568
+
569
+ if bid_strategy in strategies_requiring_bid_amount and bid_amount is None:
570
+ return json.dumps({
571
+ "error": f"bid_amount is required when using bid_strategy '{bid_strategy}'",
572
+ "details": f"The '{bid_strategy}' bid strategy requires you to specify a bid amount in cents",
573
+ "workaround": "Either provide the bid_amount parameter, or use bid_strategy='LOWEST_COST_WITHOUT_CAP' which does not require a bid amount",
574
+ "example_with_bid_amount": f'{{"bid_strategy": "{bid_strategy}", "bid_amount": 500}}',
575
+ "example_without_bid_amount": '{"bid_strategy": "LOWEST_COST_WITHOUT_CAP"}'
576
+ }, indent=2)
577
+
578
+ # LOWEST_COST_WITH_MIN_ROAS requires bid_constraints with roas_average_floor
579
+ if bid_strategy == 'LOWEST_COST_WITH_MIN_ROAS' and not bid_constraints:
580
+ return json.dumps({
581
+ "error": "bid_constraints is required when using bid_strategy 'LOWEST_COST_WITH_MIN_ROAS'",
582
+ "details": "Provide bid_constraints with roas_average_floor (target ROAS * 10000)",
583
+ "example": '{"bid_strategy": "LOWEST_COST_WITH_MIN_ROAS", "bid_constraints": {"roas_average_floor": 20000}, "optimization_goal": "VALUE"}'
584
+ }, indent=2)
585
+
586
+ params = {}
587
+
588
+ if name is not None:
589
+ params['name'] = name
590
+
591
+ if frequency_control_specs is not None:
592
+ params['frequency_control_specs'] = frequency_control_specs
593
+
594
+ if bid_strategy is not None:
595
+ params['bid_strategy'] = bid_strategy
596
+
597
+ if bid_amount is not None:
598
+ params['bid_amount'] = str(bid_amount)
599
+
600
+ if bid_constraints is not None:
601
+ params['bid_constraints'] = json.dumps(bid_constraints)
602
+
603
+ if status is not None:
604
+ params['status'] = status
605
+
606
+ if optimization_goal is not None:
607
+ params['optimization_goal'] = optimization_goal
608
+
609
+ if targeting is not None:
610
+ # Ensure proper JSON encoding for targeting
611
+ if isinstance(targeting, dict):
612
+ params['targeting'] = json.dumps(targeting)
613
+ else:
614
+ params['targeting'] = targeting # Already a string
615
+
616
+ # Add budget parameters if provided
617
+ if daily_budget is not None:
618
+ params['daily_budget'] = str(daily_budget)
619
+
620
+ if lifetime_budget is not None:
621
+ params['lifetime_budget'] = str(lifetime_budget)
622
+
623
+ if is_dynamic_creative is not None:
624
+ params['is_dynamic_creative'] = "true" if bool(is_dynamic_creative) else "false"
625
+
626
+ if start_time is not None:
627
+ params['start_time'] = start_time
628
+
629
+ if end_time is not None:
630
+ params['end_time'] = end_time
631
+
632
+ if dsa_beneficiary is not None:
633
+ params['dsa_beneficiary'] = dsa_beneficiary
634
+
635
+ if dsa_payor is not None:
636
+ params['dsa_payor'] = dsa_payor
637
+
638
+ if multi_advertiser_ads is not None:
639
+ params['multi_advertiser_ads'] = str(multi_advertiser_ads)
640
+
641
+ if regional_regulated_categories is not None:
642
+ params['regional_regulated_categories'] = json.dumps(regional_regulated_categories)
643
+
644
+ if regional_regulation_identities is not None:
645
+ params['regional_regulation_identities'] = json.dumps(regional_regulation_identities)
646
+
647
+ if attribution_spec is not None:
648
+ params['attribution_spec'] = json.dumps(attribution_spec)
649
+
650
+ if not params:
651
+ return json.dumps({"error": "No update parameters provided"}, indent=2)
652
+
653
+ endpoint = f"{adset_id}"
654
+
655
+ try:
656
+ # Use POST method for updates as per Meta API documentation
657
+ data = await make_api_request(endpoint, access_token, params, method="POST")
658
+ return json.dumps(data, indent=2)
659
+ except Exception as e:
660
+ error_msg = str(e)
661
+ # Include adset_id in error for better context
662
+ return json.dumps({
663
+ "error": f"Failed to update ad set {adset_id}",
664
+ "details": error_msg,
665
+ "params_sent": params
666
+ }, indent=2)