Meta Ads
Postgale lets your backend or AI agent run Facebook and Instagram ads through Meta's Marketing API: list ad accounts, boost a published post, build campaigns, ad sets (targeting, budget, schedule), creatives and ads, manage customer-list custom audiences and read insights. Everything is on the REST API (paths relative to https://postgale.com/api/v1) and the MCP server (tools ads_*). Calls act through a connected Facebook Page account (accountId) whose user granted ads_management / ads_read.
Safety: nothing spends by accident
- Every call that can create spend (creating a campaign, ad set or ad, boosting, setting status
ACTIVE, changing budget, bid, schedule or targeting) needs"confirm": true. Without it the answer is400 confirmation_requiredand nothing is created. - Created objects are
PAUSEDunless you send"status": "ACTIVE"together withconfirm. Pausing (PATCH … {"status":"PAUSED"}) never needs confirm. - Agents should show the plan (ad account, budget, dates, targeting) to a human and only then send
confirm. - Budgets and bids are integers in the ad account currency's minor units (cents):
2000= 20.00 USD. - When approval applies to the key, organization or account, ads writes answer
403 approval_required. Writes are audited. - Instances without
PROVIDER_LIVE_ADS=1answer from a deterministic mock ("mock": truein every response) and never call Meta.
Ad accounts
curl https://postgale.com/api/v1/ads/accounts?accountId=acc_fb_page \
-H 'Authorization: Bearer sk_…'
# {"adAccounts":[{"id":"act_1234567890","name":"Coffee Co. Ads","currency":"USD","account_status":1,…}],"mock":false}Boost a post
Turns a published Facebook Page post or Instagram post into an ad that keeps its likes and comments: Postgale creates the campaign, ad set, creative and ad. Pass postId (a Postgale post with a published Facebook or Instagram target) or platformPostId + platform. Instagram needs instagramUserId, the Instagram business account linked to the Page. goal: engagement (default), traffic, awareness, video_views, lead_generation, conversions, app_promotion. A lifetime budget needs schedule.endDate; linkUrl goes with callToAction.
curl -X POST https://postgale.com/api/v1/ads/boost \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","postId":"cmpost123","goal":"traffic","budget":{"amount":2000,"type":"daily"},"schedule":{"startDate":"2026-11-01T09:00:00Z","endDate":"2026-11-08T09:00:00Z"},"targeting":{"countries":["US","CA"],"ageMin":25,"ageMax":45,"interests":[{"id":"6003139266461","name":"Coffee"}]},"linkUrl":"https://shop.example/beans","callToAction":"SHOP_NOW","confirm":true}'{
"boost": {
"platform": "facebook",
"platformPostId": "811889972008357_1029384756",
"status": "PAUSED",
"campaignId": "120250000000000002",
"adSetId": "120250000000000003",
"creativeId": "120250000000000004",
"adId": "120260000000000001"
},
"mock": false
}Campaigns, ad sets, creatives and ads
The full tree, step by step (each create answers 201 with the new object):
curl -X POST https://postgale.com/api/v1/ads/campaigns \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","name":"Spring beans","goal":"traffic","confirm":true}'curl -X POST https://postgale.com/api/v1/ads/adsets \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","campaignId":"120250000000000002","name":"US 25-45 coffee","dailyBudget":1500,"startTime":"2026-11-01T09:00:00Z","endTime":"2026-11-15T09:00:00Z","optimizationGoal":"LINK_CLICKS","targeting":{"countries":["US"],"ageMin":25,"ageMax":45,"publisherPlatforms":["facebook","instagram"]},"confirm":true}'curl -X POST https://postgale.com/api/v1/ads/creatives \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","name":"Beans link ad","link":"https://shop.example/beans","message":"Fresh roast, shipped today","headline":"Spring beans","imageUrl":"https://media.example/beans.jpg","callToAction":"SHOP_NOW"}'curl -X POST https://postgale.com/api/v1/ads/ads \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","name":"Beans ad","adSetId":"120250000000000003","creativeId":"120250000000000004","confirm":true}'Read with GET /ads/campaigns, /ads/adsets?parentId=, /ads/ads?parentId=, /ads/creatives (?accountId&adAccountId&statuses&limit&after) and GET /ads/campaigns/{id}?accountId=. Update or pause with PATCH /ads/campaigns|adsets|ads/{id}:
curl -X PATCH https://postgale.com/api/v1/ads/campaigns/120250000000000002 \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","status":"PAUSED"}'curl -X PATCH https://postgale.com/api/v1/ads/adsets/120250000000000003 \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","dailyBudget":2500,"targeting":{"countries":["US","CA"]},"confirm":true}'Targeting
targeting takes countries, regions, cities (with radius), zips, ageMin/ageMax (13–65), genders (1 male, 2 female), interests, customAudiences/excludedCustomAudiences and publisherPlatforms; or raw, a Meta targeting spec used as is. Find interest and location keys with GET /ads/targeting/search?accountId&q=coffee&type=adinterest.
Custom audiences
curl -X POST https://postgale.com/api/v1/ads/audiences \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","adAccountId":"act_1234567890","name":"Newsletter subscribers"}'curl -X POST https://postgale.com/api/v1/ads/audiences/23850000000000001/users \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{"accountId":"acc_fb_page","emails":["ana@example.com","bo@example.com"]}'Emails and phones are normalized and SHA-256 hashed by Postgale; raw values never reach Meta. Use the audience id in targeting.customAudiences. DELETE /ads/audiences/{id}?accountId= removes it.
Insights
curl https://postgale.com/api/v1/ads/insights?accountId=acc_fb_page&objectId=120250000000000002&datePreset=last_7d&breakdowns=age,gender \
-H 'Authorization: Bearer sk_…'Rows carry impressions, reach, frequency, clicks, inline_link_clicks, spend, cpc, cpm, ctr and actions. objectId is an ad account (act_…), campaign, ad set or ad; since/until (YYYY-MM-DD), level, timeIncrement=1 for daily rows.
Permissions
The Facebook account must be (re)connected after the Meta app has Advanced Access to ads_management and ads_read (plus business_management, pages_manage_ads for boosting). Postgale keeps the connecting user's token encrypted for ads calls; it is never returned. Self-hosted: set FACEBOOK_ADS_SCOPES (classic login) or FACEBOOK_ADS_CONFIG_ID (Login for Business), then PROVIDER_LIVE_ADS=1.
Errors
| Status | code | When |
|---|---|---|
| 400 | confirmation_required | A spend-capable call (create, boost, ACTIVE, budget, bid, schedule or targeting change) without confirm: true. Nothing was created or changed |
| 400 | invalid_request | A field failed validation (budget below 100 minor units, lifetime budget without endTime, unknown goal…) |
| 400 | not_supported | accountId is not a Facebook Page account, or the object level cannot be updated |
| 403 | approval_required | Approval applies to the key, organization or account: ads writes are refused |
| 403 | missing_permission | The Facebook account did not grant ads_management / ads_read: reconnect it after the app has the permissions |
| 409 | reconnect_required | The token is no longer valid: reconnect the Facebook account |
| 429 | rate_limited | Meta’s Marketing API rate limits (development tier is low) |
| 502 | platform_error | Any other Meta failure (the message carries Meta’s reason) |