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,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)
|