# Facebook Ads API: Create a Campaign, Ad Set, and Ad

Create a Facebook ad campaign with the Marketing API in four curl calls: campaign, ad set, creative, ad. Covers objectives, special ad categories, and budgets.

Canonical page: https://www.tryadeli.com/blog/facebook-ads-api-create-campaign
Last updated: 2026-10-26

By Nick Reyes, Co-founder and CTO, Adeli. Published 2026-10-26.

## The short answer

To create a campaign with the Facebook Ads API, make four Marketing API calls in order: POST /act_{id}/campaigns with an OUTCOME_ objective and special_ad_categories, then /adsets with budget and targeting, /adcreatives, and /ads. Create everything PAUSED and activate once it's checked. Budgets are in the currency's smallest unit, such as cents. Adeli builds the whole chain from a Page post in one paused call.

To create a campaign with the Facebook Ads API, you make four calls to Meta's Marketing API in order: a **campaign** (the objective), an **ad set** (budget, schedule, and targeting), an **ad creative** (what people see), and an **ad** that joins the creative to the ad set. Every campaign needs `special_ad_categories`, new campaigns must use an `OUTCOME_` objective, and the safe default is to create everything with `status=PAUSED`.

This guide walks through each call with curl, for developers at platforms that launch ads on their customers' behalf. It's part of our [Meta Ads API](https://www.tryadeli.com/blog/meta-ads-api) series.

## What do you need before you start?

- **An ad account ID** in `act_<digits>` form. Our guide to [creating an ad account with the Meta API](https://www.tryadeli.com/blog/create-ad-account-meta-api) covers making one per customer.
- **A token with `ads_management`**, from a person or system user who can advertise on that ad account.
- **A Facebook Page** the ads will run as, with `pages_manage_ads` if you promote its posts.
- **A payment method on the ad account.** Without one, everything below gets created but nothing delivers.

## How is a Facebook ad campaign structured?

**A Facebook ad campaign is a three-level tree: one campaign holds ad sets, and each ad set holds ads.** Each level owns different settings, which is why it takes four calls.

| Object | Endpoint | Owns |
|---|---|---|
| Campaign | `POST /act_{id}/campaigns` | Objective, special ad categories, optional campaign budget |
| Ad set | `POST /act_{id}/adsets` | Budget (unless the campaign has one), schedule, targeting, optimization goal, billing event |
| Ad creative | `POST /act_{id}/adcreatives` | The post, image, video, text, and call to action |
| Ad | `POST /act_{id}/ads` | Links one creative to one ad set |

## Step 1: Create the campaign

**Send a POST to the ad account's `campaigns` edge with a name, an objective, `special_ad_categories`, and `status=PAUSED`.** Meta's [getting started guide](https://developers.facebook.com/docs/marketing-api/get-started/basic-ad-creation/create-an-ad-campaign) says to set `PAUSED` "to keep the campaign inactive until you finish setup."

```bash
curl -X POST "https://graph.facebook.com/v26.0/act_$AD_ACCOUNT_ID/campaigns" \
  -F "name=Fall launch" \
  -F "objective=OUTCOME_TRAFFIC" \
  -F "special_ad_categories=[]" \
  -F "status=PAUSED" \
  -F "access_token=$ACCESS_TOKEN"
```

The response is `{"id": "<CAMPAIGN_ID>"}`. Only `ACTIVE` and `PAUSED` are valid at creation; `DELETED` and `ARCHIVED` are for updates.

## Which objective should I use?

**Use one of the six outcome objectives, because new campaigns can't use the original ones.** Meta moved campaign creation to these simplified objectives, sometimes called ODAX (outcome-driven ad experiences), in Marketing API v17.0:

> "Beginning with Marketing API v17.0 (to be released in Spring 2023), campaigns can only be created with simplified objectives."
> Jay Wei, Meta for Developers blog, [February 13, 2023](https://developers.facebook.com/blog/post/2023/02/13/outcome-driven-ad-experiences-update/)

That retired 15 original objectives for new campaigns, including `LINK_CLICKS`, `CONVERSIONS`, `REACH`, and `LEAD_GENERATION`. Older tutorials still use them, and the call fails. The current set, from Meta's [campaigns reference](https://developers.facebook.com/docs/marketing-api/reference/ad-account/campaigns/):

| Objective | Use it when the customer wants |
|---|---|
| `OUTCOME_AWARENESS` | Reach and ad recall |
| `OUTCOME_TRAFFIC` | Visits to a site or app |
| `OUTCOME_ENGAGEMENT` | Post engagement, video views, messages |
| `OUTCOME_LEADS` | Leads from instant forms, calls, or messages |
| `OUTCOME_APP_PROMOTION` | App installs and in-app events |
| `OUTCOME_SALES` | Purchases and other conversions |

**Special ad categories** are the other required field. Meta's [special ad category guide](https://developers.facebook.com/docs/marketing-api/audiences/special-ad-category) lists `HOUSING`, `EMPLOYMENT`, `FINANCIAL_PRODUCTS_SERVICES`, and `ISSUES_ELECTIONS_POLITICS`. Any category restricts targeting and needs `special_ad_category_country`. For a vertical SaaS serving real estate agents or recruiters, ask for this in your UI rather than defaulting to `NONE`.

## Step 2: Create the ad set

**Create the ad set with the campaign ID, a budget, a schedule, targeting, an optimization goal, and a billing event.** Check the budget unit first when this call misbehaves.

```bash
curl -X POST "https://graph.facebook.com/v26.0/act_$AD_ACCOUNT_ID/adsets" \
  -F "name=Fall launch - US" \
  -F "campaign_id=$CAMPAIGN_ID" \
  -F "daily_budget=2500" \
  -F "bid_strategy=LOWEST_COST_WITHOUT_CAP" \
  -F "billing_event=IMPRESSIONS" \
  -F "optimization_goal=LINK_CLICKS" \
  -F "start_time=2026-11-01T09:00:00-0500" \
  -F "end_time=2026-11-08T09:00:00-0500" \
  -F 'targeting={"geo_locations":{"countries":["US"]}}' \
  -F "status=PAUSED" \
  -F "access_token=$ACCESS_TOKEN"
```

- **Budgets are in the smallest currency unit.** Meta's [budgets page](https://developers.facebook.com/docs/marketing-api/bidding/overview/budgets) says amounts are at the account currency's "minimum denomination level, such as cents for USD", so `2500` is $25.00 a day.
- **Meta can overspend a day.** On days with better opportunities, Meta may spend up to 25% more than the daily budget.
- **A lifetime budget needs an end time.** `daily_budget` is allowed only for ad sets that run longer than 24 hours.
- **Set the bid strategy on purpose.** A capped strategy such as `LOWEST_COST_WITH_BID_CAP` needs a `bid_amount` on every ad set.

## Should the budget sit on the campaign or the ad set?

**Put the budget on the campaign when you want Meta to move money between ad sets, and on each ad set when your customer needs fixed amounts.** Meta's reference says a budget can be set on the campaign or on its ad sets, not both.

| | Campaign budget (Advantage campaign budget, formerly CBO) | Ad set budgets |
|---|---|---|
| Where | `daily_budget` or `lifetime_budget` on the campaign | `daily_budget` or `lifetime_budget` on each ad set |
| Who splits the money | Meta, in real time, toward the best results | You |
| Bid strategy | Set on the campaign | Set on each ad set |
| Best for | One goal, several audiences | Per-audience spend your customer has promised |

For a platform, ad set budgets are easier to explain in a UI ("$25 a day for this audience").

## Step 3: Create the ad creative

**Create a creative that points at an existing Page post with `object_story_id`, or describes a new one with `object_story_spec`.** Promoting a post the customer already published is the simplest path:

```bash
curl -X POST "https://graph.facebook.com/v26.0/act_$AD_ACCOUNT_ID/adcreatives" \
  -F "name=Fall launch creative" \
  -F "object_story_id=${PAGE_ID}_${POST_ID}" \
  -F "access_token=$ACCESS_TOKEN"
```

The `object_story_id` is the Page ID and the post ID joined by an underscore. Meta's [ad creatives reference](https://developers.facebook.com/docs/marketing-api/reference/ad-account/adcreatives/) notes that reusing a post already attached to a creative returns that creative's ID instead of making a new one.

## Step 4: Create the ad

**Create the ad with the ad set ID and the creative ID, still paused.**

```bash
curl -X POST "https://graph.facebook.com/v26.0/act_$AD_ACCOUNT_ID/ads" \
  -F "name=Fall launch ad" \
  -F "adset_id=$AD_SET_ID" \
  -F 'creative={"creative_id":"'"$CREATIVE_ID"'"}' \
  -F "status=PAUSED" \
  -F "access_token=$ACCESS_TOKEN"
```

To launch, set `status=ACTIVE` on the campaign, the ad set, and the ad. Pausing any parent pauses its children: their `effective_status` reads `CAMPAIGN_PAUSED` or `ADSET_PAUSED`.

## How do you know it worked?

**Read the ad's `effective_status`.** `GET /{ad-id}?fields=status,effective_status` tells you where it stands:

- **`PENDING_REVIEW`.** Meta is reviewing the ad. Nothing delivers until it passes.
- **`DISAPPROVED`.** The ad broke an advertising policy; show the customer the reason.
- **`PENDING_BILLING_INFO`.** The ad account has no working payment method.
- **`ACTIVE`.** Delivering. Spend and results show up in the [Facebook Ads Insights API](https://www.tryadeli.com/blog/facebook-ads-insights-api).

If a later call fails, delete the objects the earlier calls created. Otherwise paused orphans pile up in the customer's Ads Manager.

## Is there a faster way to do this?

**Adeli creates the campaign, ad set, creative, and ad from one call, all paused, by promoting a post the customer's Page already published.** Adeli holds the Meta app and the `ads_management` and `pages_manage_ads` grants, so you skip App Review. The endpoint is [create campaign](https://www.tryadeli.com/docs/api/ads/create-campaign):

```bash
curl -X POST "https://app.tryadeli.com/api/v1/ads/campaigns?accountId=$ACCOUNT_ID" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Idempotency-Key: fall-launch-boost" \
  -H "Content-Type: application/json" \
  -d '{"adAccountId":"act_1234567890","pagePostId":"987654321098765","dailyBudget":25,"startDate":"2026-11-01","endDate":"2026-11-07","campaignName":"Fall launch"}'
```

What the endpoint handles for you:

- **Whole currency units.** Adeli's `dailyBudget` takes `25` for 25.00.
- **Idempotent retries.** The same `Idempotency-Key` replays the original result rather than creating a second campaign. Adeli's docs call this the one call where a retry has a cost.
- **Rollback.** If a step fails, Adeli deletes what it already created and returns the failing `step`, plus any `orphanedObjectIds` it couldn't remove.
- **A preview.** The response includes Meta's ad preview URL when Meta returns one, so your customer can see the ad before it spends.

Then [update status](https://www.tryadeli.com/docs/api/ads/update-status) sets the ad or ad set to `ACTIVE`, [campaign tree](https://www.tryadeli.com/docs/api/ads/get-campaign-tree) returns campaigns, ad sets, and ads with spend and clicks at every level, and [delete campaign](https://www.tryadeli.com/docs/api/ads/delete-campaign) removes a campaign with its children. The same API key [publishes the posts](https://www.tryadeli.com/social-media-posting-api) you promote, and [pricing](https://www.tryadeli.com/pricing) is per customer profile.

## Frequently asked questions

### Is special_ad_categories required on every campaign?

Yes. Meta requires special_ad_categories on every campaign created through the Marketing API. Send NONE or an empty array when the ads are not about housing, employment, financial products and services, or social issues, elections, and politics. Picking a category restricts the targeting options available.

### Can I still use the LINK_CLICKS or CONVERSIONS objective?

Not for new campaigns. Since Marketing API v17.0, new campaigns must use the outcome objectives, such as OUTCOME_TRAFFIC and OUTCOME_SALES. Campaigns created earlier with the original objectives keep running and can still be duplicated.

### Why is my Facebook ad stuck in PENDING_REVIEW?

Meta reviews ads before they deliver, and the ad's effective_status reads PENDING_REVIEW while that happens. If it moves to DISAPPROVED, the ad broke an advertising policy. PENDING_BILLING_INFO means the ad account has no working payment method.

### How do I delete a campaign through the API?

Send a DELETE request to the campaign ID, or update its status to DELETED. Deleting a campaign removes its ad sets and ads with it and stops delivery immediately, and it can't be undone.

## Related Adeli pages

- [Social Media and Ads API for AI Marketing Platforms](https://www.tryadeli.com/use-cases/ai-marketing-platforms.md): Run posting, analytics, inbox, and Meta ads for your customers' accounts through one API, without months of platform reviews.
- [Social Media API for Vertical SaaS Platforms](https://www.tryadeli.com/use-cases/vertical-saas.md): Give your small-business customers posting, scheduling, analytics, inbox, and Meta ads inside your vertical SaaS, through one API.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Nick Reyes is the CTO behind Adeli and builds its API, its platform integrations, and the docs. He is a product engineer with 10 years of experience at Phantom, Airbnb, and Google.
