# Meta Ads

Source: https://staging.postgale.com/docs/ads

Boost Facebook and Instagram posts and manage Meta campaigns, ad sets, ads, audiences and insights through the Postgale REST API and MCP server.

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](https://staging.postgale.com/docs/api) (paths relative to `https://staging.postgale.com/api/v1`) and the
[MCP server](https://staging.postgale.com/docs/mcp) (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 is `400 confirmation_required` and nothing is created.
- Created objects are `PAUSED` unless you send `"status": "ACTIVE"` together with `confirm`. 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=1` answer from a deterministic mock (`"mock": true` in every response) and never call Meta.

## Ad accounts

GET /api/v1/ads/accounts:
```bash
curl https://staging.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`.

POST /api/v1/ads/boost:
```bash
curl -X POST https://staging.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}'
```

```json
{
  "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):

POST /api/v1/ads/campaigns:
```bash
curl -X POST https://staging.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}'
```

POST /api/v1/ads/adsets:
```bash
curl -X POST https://staging.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}'
```

POST /api/v1/ads/creatives:
```bash
curl -X POST https://staging.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"}'
```

POST /api/v1/ads/ads:
```bash
curl -X POST https://staging.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}`:

PATCH /api/v1/ads/campaigns/{id}:
```bash
curl -X PATCH https://staging.postgale.com/api/v1/ads/campaigns/120250000000000002 \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"accountId":"acc_fb_page","status":"PAUSED"}'
```

```bash
curl -X PATCH https://staging.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

POST /api/v1/ads/audiences:
```bash
curl -X POST https://staging.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"}'
```

POST /api/v1/ads/audiences/{id}/users:
```bash
curl -X POST https://staging.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

GET /api/v1/ads/insights:
```bash
curl https://staging.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.

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