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.
- meta_ads_mcp/__init__.py +79 -0
- meta_ads_mcp/__main__.py +10 -0
- meta_ads_mcp/core/__init__.py +55 -0
- meta_ads_mcp/core/accounts.py +141 -0
- meta_ads_mcp/core/ads.py +2751 -0
- meta_ads_mcp/core/ads_library.py +74 -0
- meta_ads_mcp/core/adsets.py +666 -0
- meta_ads_mcp/core/api.py +431 -0
- meta_ads_mcp/core/auth.py +567 -0
- meta_ads_mcp/core/authentication.py +207 -0
- meta_ads_mcp/core/budget_schedules.py +70 -0
- meta_ads_mcp/core/callback_server.py +256 -0
- meta_ads_mcp/core/campaigns.py +379 -0
- meta_ads_mcp/core/duplication.py +523 -0
- meta_ads_mcp/core/http_auth_integration.py +307 -0
- meta_ads_mcp/core/insights.py +161 -0
- meta_ads_mcp/core/mcc.py +232 -0
- meta_ads_mcp/core/openai_deep_research.py +418 -0
- meta_ads_mcp/core/pipeboard_auth.py +510 -0
- meta_ads_mcp/core/reports.py +135 -0
- meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp/core/server.py +391 -0
- meta_ads_mcp/core/targeting.py +542 -0
- meta_ads_mcp/core/utils.py +225 -0
- meta_ads_mcp/settings.py +33 -0
- meta_ads_mcp_python-1.0.79.dist-info/METADATA +187 -0
- meta_ads_mcp_python-1.0.79.dist-info/RECORD +29 -0
- meta_ads_mcp_python-1.0.79.dist-info/WHEEL +4 -0
- meta_ads_mcp_python-1.0.79.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Campaign-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Optional, Dict, Any, Union
|
|
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_campaigns(
|
|
13
|
+
account_id: str,
|
|
14
|
+
access_token: Optional[str] = None,
|
|
15
|
+
limit: int = 10,
|
|
16
|
+
status_filter: str = "",
|
|
17
|
+
objective_filter: Union[str, List[str]] = "",
|
|
18
|
+
after: str = ""
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Get campaigns for a Meta Ads account with optional filtering.
|
|
22
|
+
|
|
23
|
+
Note: By default, the Meta API returns a subset of available fields.
|
|
24
|
+
Other fields like 'effective_status', 'spend_cap', 'budget_remaining',
|
|
25
|
+
'promoted_object', 'source_campaign_id', etc., might be available but
|
|
26
|
+
require specifying them in the API call (currently not exposed by this
|
|
27
|
+
tool's parameters).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
31
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
32
|
+
limit: Maximum number of campaigns to return (default: 10)
|
|
33
|
+
status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
|
|
34
|
+
Maps to the 'effective_status' API parameter, which expects an array
|
|
35
|
+
(this function handles the required JSON formatting). Leave empty for all statuses.
|
|
36
|
+
objective_filter: Filter by campaign objective(s). Can be a single objective string or a list of objectives.
|
|
37
|
+
Valid objectives: 'OUTCOME_AWARENESS', 'OUTCOME_TRAFFIC', 'OUTCOME_ENGAGEMENT',
|
|
38
|
+
'OUTCOME_LEADS', 'OUTCOME_SALES', 'OUTCOME_APP_PROMOTION'.
|
|
39
|
+
Examples: 'OUTCOME_LEADS' or ['OUTCOME_LEADS', 'OUTCOME_SALES'].
|
|
40
|
+
Leave empty for all objectives.
|
|
41
|
+
after: Pagination cursor to get the next set of results
|
|
42
|
+
"""
|
|
43
|
+
# Require explicit account_id
|
|
44
|
+
if not account_id:
|
|
45
|
+
return json.dumps({"error": "No account ID specified"}, indent=2)
|
|
46
|
+
|
|
47
|
+
account_id = ensure_act_prefix(account_id)
|
|
48
|
+
endpoint = f"{account_id}/campaigns"
|
|
49
|
+
params = {
|
|
50
|
+
"fields": "id,name,objective,status,daily_budget,lifetime_budget,buying_type,start_time,stop_time,created_time,updated_time,bid_strategy,special_ad_categories",
|
|
51
|
+
"limit": limit
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Build filtering array for complex filtering
|
|
55
|
+
filters = []
|
|
56
|
+
|
|
57
|
+
if status_filter:
|
|
58
|
+
# API expects an array, encode it as a JSON string
|
|
59
|
+
params["effective_status"] = json.dumps([status_filter])
|
|
60
|
+
|
|
61
|
+
# Handle objective filtering - supports both single string and list of objectives
|
|
62
|
+
if objective_filter:
|
|
63
|
+
# Convert single string to list for consistent handling
|
|
64
|
+
objectives = [objective_filter] if isinstance(objective_filter, str) else objective_filter
|
|
65
|
+
|
|
66
|
+
# Filter out empty strings
|
|
67
|
+
objectives = [obj for obj in objectives if obj]
|
|
68
|
+
|
|
69
|
+
if objectives:
|
|
70
|
+
filters.append({
|
|
71
|
+
"field": "objective",
|
|
72
|
+
"operator": "IN",
|
|
73
|
+
"value": objectives
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
# Add filtering parameter if we have filters
|
|
77
|
+
if filters:
|
|
78
|
+
params["filtering"] = json.dumps(filters)
|
|
79
|
+
|
|
80
|
+
if after:
|
|
81
|
+
params["after"] = after
|
|
82
|
+
|
|
83
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
84
|
+
|
|
85
|
+
return json.dumps(data, indent=2)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp_server.tool()
|
|
89
|
+
@meta_api_tool
|
|
90
|
+
async def get_campaign_details(campaign_id: str, access_token: Optional[str] = None) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Get detailed information about a specific campaign.
|
|
93
|
+
|
|
94
|
+
Note: This function requests a specific set of fields ('id,name,objective,status,...').
|
|
95
|
+
The Meta API offers many other fields for campaigns (e.g., 'effective_status', 'source_campaign_id', etc.)
|
|
96
|
+
that could be added to the 'fields' parameter in the code if needed.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
campaign_id: Meta Ads campaign ID
|
|
100
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
101
|
+
"""
|
|
102
|
+
if not campaign_id:
|
|
103
|
+
return json.dumps({"error": "No campaign ID provided"}, indent=2)
|
|
104
|
+
|
|
105
|
+
endpoint = f"{campaign_id}"
|
|
106
|
+
params = {
|
|
107
|
+
"fields": "id,name,objective,status,daily_budget,lifetime_budget,buying_type,start_time,stop_time,created_time,updated_time,bid_strategy,special_ad_categories,special_ad_category_country,budget_remaining,configured_status"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
111
|
+
|
|
112
|
+
return json.dumps(data, indent=2)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@meta_api_tool
|
|
116
|
+
async def create_campaign(
|
|
117
|
+
account_id: str,
|
|
118
|
+
name: str,
|
|
119
|
+
objective: str,
|
|
120
|
+
access_token: Optional[str] = None,
|
|
121
|
+
status: str = "PAUSED",
|
|
122
|
+
special_ad_categories: Optional[List[str]] = None,
|
|
123
|
+
daily_budget: Optional[int] = None,
|
|
124
|
+
lifetime_budget: Optional[int] = None,
|
|
125
|
+
buying_type: Optional[str] = None,
|
|
126
|
+
bid_strategy: str = "LOWEST_COST_WITHOUT_CAP",
|
|
127
|
+
bid_cap: Optional[int] = None,
|
|
128
|
+
spend_cap: Optional[int] = None,
|
|
129
|
+
campaign_budget_optimization: Optional[bool] = None,
|
|
130
|
+
ab_test_control_setups: Optional[List[Dict[str, Any]]] = None,
|
|
131
|
+
use_adset_level_budgets: bool = False
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Create a new campaign in a Meta Ads account.
|
|
135
|
+
|
|
136
|
+
Note: Campaigns do not support start_time for scheduling -- set start_time on the ad set instead.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
140
|
+
name: Campaign name
|
|
141
|
+
objective: Campaign objective (ODAX, outcome-based). Must be one of:
|
|
142
|
+
OUTCOME_AWARENESS, OUTCOME_TRAFFIC, OUTCOME_ENGAGEMENT,
|
|
143
|
+
OUTCOME_LEADS, OUTCOME_SALES, OUTCOME_APP_PROMOTION.
|
|
144
|
+
Note: Legacy objectives like BRAND_AWARENESS, LINK_CLICKS,
|
|
145
|
+
CONVERSIONS, APP_INSTALLS, etc. are not valid for new
|
|
146
|
+
campaigns and will cause a 400 error. Use the outcome-based
|
|
147
|
+
values above (e.g., BRAND_AWARENESS -> OUTCOME_AWARENESS).
|
|
148
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
149
|
+
status: Initial campaign status (default: PAUSED)
|
|
150
|
+
special_ad_categories: List of special ad categories if applicable
|
|
151
|
+
daily_budget: Daily budget in account currency (in cents) as a string (only used if use_adset_level_budgets=False)
|
|
152
|
+
lifetime_budget: Lifetime budget in account currency (in cents) as a string (only used if use_adset_level_budgets=False)
|
|
153
|
+
buying_type: Buying type (e.g., 'AUCTION')
|
|
154
|
+
bid_strategy: Bid strategy (default: LOWEST_COST_WITHOUT_CAP). Must be one of: 'LOWEST_COST_WITHOUT_CAP', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP', 'LOWEST_COST_WITH_MIN_ROAS'. WARNING: If you use LOWEST_COST_WITH_BID_CAP or COST_CAP, all child ad sets will require bid_amount to be set.
|
|
155
|
+
bid_cap: Bid cap in account currency (in cents) as a string
|
|
156
|
+
spend_cap: Spending limit for the campaign in account currency (in cents) as a string
|
|
157
|
+
campaign_budget_optimization: Whether to enable campaign budget optimization (only used if use_adset_level_budgets=False)
|
|
158
|
+
ab_test_control_setups: Settings for A/B testing (e.g., [{"name":"Creative A", "ad_format":"SINGLE_IMAGE"}])
|
|
159
|
+
use_adset_level_budgets: If True, budgets will be set at the ad set level instead of campaign level (default: False)
|
|
160
|
+
"""
|
|
161
|
+
# Check required parameters
|
|
162
|
+
if not account_id:
|
|
163
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
164
|
+
|
|
165
|
+
if not name:
|
|
166
|
+
return json.dumps({"error": "No campaign name provided"}, indent=2)
|
|
167
|
+
|
|
168
|
+
if not objective:
|
|
169
|
+
return json.dumps({"error": "No campaign objective provided"}, indent=2)
|
|
170
|
+
|
|
171
|
+
account_id = ensure_act_prefix(account_id)
|
|
172
|
+
|
|
173
|
+
# Track whether the user explicitly provided special_ad_categories
|
|
174
|
+
_user_provided_categories = special_ad_categories is not None
|
|
175
|
+
|
|
176
|
+
# Special_ad_categories is required by the API, set default if not provided
|
|
177
|
+
if special_ad_categories is None:
|
|
178
|
+
special_ad_categories = []
|
|
179
|
+
|
|
180
|
+
# Only warn if user omitted special_ad_categories entirely.
|
|
181
|
+
# If they explicitly passed [] they are saying none are needed.
|
|
182
|
+
compliance_warning = None
|
|
183
|
+
if objective == "OUTCOME_LEADS" and not special_ad_categories and not _user_provided_categories:
|
|
184
|
+
compliance_warning = (
|
|
185
|
+
"Warning: Campaign objective is OUTCOME_LEADS but no special_ad_categories were specified. "
|
|
186
|
+
"If this campaign is for a regulated industry (insurance, housing, employment, credit), "
|
|
187
|
+
"you must set special_ad_categories (e.g., FINANCIAL_PRODUCTS_SERVICES, HOUSING, EMPLOYMENT, CREDIT) "
|
|
188
|
+
"to comply with Meta advertising policies. Ads without the correct category may be rejected."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# For this example, we'll add a fixed daily budget if none is provided and we're not using ad set level budgets
|
|
192
|
+
if not daily_budget and not lifetime_budget and not use_adset_level_budgets:
|
|
193
|
+
daily_budget = "1000" # Default to $10 USD
|
|
194
|
+
|
|
195
|
+
endpoint = f"{account_id}/campaigns"
|
|
196
|
+
|
|
197
|
+
params = {
|
|
198
|
+
"name": name,
|
|
199
|
+
"objective": objective,
|
|
200
|
+
"status": status,
|
|
201
|
+
"special_ad_categories": json.dumps(special_ad_categories) # Properly format as JSON string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Only set campaign-level budgets if we're not using ad set level budgets
|
|
205
|
+
if not use_adset_level_budgets:
|
|
206
|
+
# Convert budget values to strings if they aren't already
|
|
207
|
+
if daily_budget is not None:
|
|
208
|
+
params["daily_budget"] = str(daily_budget)
|
|
209
|
+
|
|
210
|
+
if lifetime_budget is not None:
|
|
211
|
+
params["lifetime_budget"] = str(lifetime_budget)
|
|
212
|
+
|
|
213
|
+
if campaign_budget_optimization is not None:
|
|
214
|
+
params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
|
|
215
|
+
else:
|
|
216
|
+
# Meta API v24 requires is_adset_budget_sharing_enabled when not using campaign budget
|
|
217
|
+
params["is_adset_budget_sharing_enabled"] = "false"
|
|
218
|
+
|
|
219
|
+
# Add new parameters
|
|
220
|
+
if buying_type:
|
|
221
|
+
params["buying_type"] = buying_type
|
|
222
|
+
|
|
223
|
+
if bid_strategy and not use_adset_level_budgets:
|
|
224
|
+
params["bid_strategy"] = bid_strategy
|
|
225
|
+
|
|
226
|
+
if bid_cap is not None:
|
|
227
|
+
params["bid_cap"] = str(bid_cap)
|
|
228
|
+
|
|
229
|
+
if spend_cap is not None:
|
|
230
|
+
params["spend_cap"] = str(spend_cap)
|
|
231
|
+
|
|
232
|
+
if ab_test_control_setups:
|
|
233
|
+
params["ab_test_control_setups"] = json.dumps(ab_test_control_setups)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
237
|
+
|
|
238
|
+
# Add a note about budget strategy if using ad set level budgets
|
|
239
|
+
if use_adset_level_budgets:
|
|
240
|
+
data["budget_strategy"] = "ad_set_level"
|
|
241
|
+
data["note"] = "Campaign created with ad set level budgets. Set budgets when creating ad sets within this campaign."
|
|
242
|
+
|
|
243
|
+
if compliance_warning:
|
|
244
|
+
data["compliance_warning"] = compliance_warning
|
|
245
|
+
|
|
246
|
+
return json.dumps(data, indent=2)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
error_msg = str(e)
|
|
249
|
+
return json.dumps({
|
|
250
|
+
"error": "Failed to create campaign",
|
|
251
|
+
"details": error_msg,
|
|
252
|
+
"params_sent": params
|
|
253
|
+
}, indent=2)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@meta_api_tool
|
|
257
|
+
async def update_campaign(
|
|
258
|
+
campaign_id: str,
|
|
259
|
+
access_token: Optional[str] = None,
|
|
260
|
+
name: Optional[str] = None,
|
|
261
|
+
status: Optional[str] = None,
|
|
262
|
+
special_ad_categories: Optional[List[str]] = None,
|
|
263
|
+
daily_budget: Optional[int] = None,
|
|
264
|
+
lifetime_budget: Optional[int] = None,
|
|
265
|
+
bid_strategy: Optional[str] = None,
|
|
266
|
+
bid_cap: Optional[int] = None,
|
|
267
|
+
spend_cap: Optional[int] = None,
|
|
268
|
+
campaign_budget_optimization: Optional[bool] = None,
|
|
269
|
+
objective: Optional[str] = None, # Add objective if it's updatable
|
|
270
|
+
use_adset_level_budgets: Optional[bool] = None, # Add other updatable fields as needed based on API docs
|
|
271
|
+
) -> str:
|
|
272
|
+
"""
|
|
273
|
+
Update an existing campaign in a Meta Ads account.
|
|
274
|
+
|
|
275
|
+
Note: Campaigns do not support start_time for scheduling -- set start_time on the ad set instead.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
campaign_id: Meta Ads campaign ID
|
|
279
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
280
|
+
name: New campaign name
|
|
281
|
+
status: New campaign status (e.g., 'ACTIVE', 'PAUSED')
|
|
282
|
+
special_ad_categories: List of special ad categories if applicable
|
|
283
|
+
daily_budget: New daily budget in account currency (in cents) as a string.
|
|
284
|
+
Set to empty string "" to remove the daily budget.
|
|
285
|
+
lifetime_budget: New lifetime budget in account currency (in cents) as a string.
|
|
286
|
+
Set to empty string "" to remove the lifetime budget.
|
|
287
|
+
bid_strategy: New bid strategy
|
|
288
|
+
bid_cap: New bid cap in account currency (in cents) as a string
|
|
289
|
+
spend_cap: New spending limit for the campaign in account currency (in cents) as a string
|
|
290
|
+
campaign_budget_optimization: Enable/disable campaign budget optimization
|
|
291
|
+
objective: New campaign objective (Note: May not always be updatable)
|
|
292
|
+
use_adset_level_budgets: If True, removes campaign-level budgets to switch to ad set level budgets
|
|
293
|
+
"""
|
|
294
|
+
if not campaign_id:
|
|
295
|
+
return json.dumps({"error": "No campaign ID provided"}, indent=2)
|
|
296
|
+
|
|
297
|
+
endpoint = f"{campaign_id}"
|
|
298
|
+
|
|
299
|
+
params = {}
|
|
300
|
+
|
|
301
|
+
# Add parameters to the request only if they are provided
|
|
302
|
+
if name is not None:
|
|
303
|
+
params["name"] = name
|
|
304
|
+
if status is not None:
|
|
305
|
+
params["status"] = status
|
|
306
|
+
if special_ad_categories is not None:
|
|
307
|
+
# Note: Updating special_ad_categories might have specific API rules or might not be allowed after creation.
|
|
308
|
+
# The API might require an empty list `[]` to clear categories. Check Meta Docs.
|
|
309
|
+
params["special_ad_categories"] = json.dumps(special_ad_categories)
|
|
310
|
+
|
|
311
|
+
# Handle budget parameters based on use_adset_level_budgets setting
|
|
312
|
+
if use_adset_level_budgets is not None:
|
|
313
|
+
if use_adset_level_budgets:
|
|
314
|
+
# Remove campaign-level budgets when switching to ad set level budgets
|
|
315
|
+
params["daily_budget"] = ""
|
|
316
|
+
params["lifetime_budget"] = ""
|
|
317
|
+
if campaign_budget_optimization is not None:
|
|
318
|
+
params["campaign_budget_optimization"] = "false"
|
|
319
|
+
else:
|
|
320
|
+
# If switching back to campaign-level budgets, use the provided budget values
|
|
321
|
+
if daily_budget is not None:
|
|
322
|
+
if daily_budget == "":
|
|
323
|
+
params["daily_budget"] = ""
|
|
324
|
+
else:
|
|
325
|
+
params["daily_budget"] = str(daily_budget)
|
|
326
|
+
if lifetime_budget is not None:
|
|
327
|
+
if lifetime_budget == "":
|
|
328
|
+
params["lifetime_budget"] = ""
|
|
329
|
+
else:
|
|
330
|
+
params["lifetime_budget"] = str(lifetime_budget)
|
|
331
|
+
if campaign_budget_optimization is not None:
|
|
332
|
+
params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
|
|
333
|
+
else:
|
|
334
|
+
# Normal budget updates when not changing budget strategy
|
|
335
|
+
if daily_budget is not None:
|
|
336
|
+
# To remove budget, set to empty string
|
|
337
|
+
if daily_budget == "":
|
|
338
|
+
params["daily_budget"] = ""
|
|
339
|
+
else:
|
|
340
|
+
params["daily_budget"] = str(daily_budget)
|
|
341
|
+
if lifetime_budget is not None:
|
|
342
|
+
# To remove budget, set to empty string
|
|
343
|
+
if lifetime_budget == "":
|
|
344
|
+
params["lifetime_budget"] = ""
|
|
345
|
+
else:
|
|
346
|
+
params["lifetime_budget"] = str(lifetime_budget)
|
|
347
|
+
if campaign_budget_optimization is not None:
|
|
348
|
+
params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
|
|
349
|
+
|
|
350
|
+
if bid_strategy is not None:
|
|
351
|
+
params["bid_strategy"] = bid_strategy
|
|
352
|
+
if bid_cap is not None:
|
|
353
|
+
params["bid_cap"] = str(bid_cap)
|
|
354
|
+
if spend_cap is not None:
|
|
355
|
+
params["spend_cap"] = str(spend_cap)
|
|
356
|
+
if objective is not None:
|
|
357
|
+
params["objective"] = objective # Caution: Objective changes might reset learning or be restricted
|
|
358
|
+
|
|
359
|
+
if not params:
|
|
360
|
+
return json.dumps({"error": "No update parameters provided"}, indent=2)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
# Use POST method for updates as per Meta API documentation
|
|
364
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
365
|
+
|
|
366
|
+
# Add a note about budget strategy if switching to ad set level budgets
|
|
367
|
+
if use_adset_level_budgets is not None and use_adset_level_budgets:
|
|
368
|
+
data["budget_strategy"] = "ad_set_level"
|
|
369
|
+
data["note"] = "Campaign updated to use ad set level budgets. Set budgets when creating ad sets within this campaign."
|
|
370
|
+
|
|
371
|
+
return json.dumps(data, indent=2)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
error_msg = str(e)
|
|
374
|
+
# Include campaign_id in error for better context
|
|
375
|
+
return json.dumps({
|
|
376
|
+
"error": f"Failed to update campaign {campaign_id}",
|
|
377
|
+
"details": error_msg,
|
|
378
|
+
"params_sent": params # Be careful about logging sensitive data if any
|
|
379
|
+
}, indent=2)
|