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

By Nick ReyesPublished 5 min read

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 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 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 says to set PAUSED "to keep the campaign inactive until you finish setup."

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

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:

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 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.

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 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:

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 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.

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.

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:

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 sets the ad or ad set to ACTIVE, campaign tree returns campaigns, ad sets, and ads with spend and clicks at every level, and delete campaign removes a campaign with its children. The same API key publishes the posts you promote, and pricing is per customer profile.

faq

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.

Power your next project
with one ads & social media API

Give your users publishing, scheduling, and analytics through one integration your team can actually maintain.

Try with agents

Start with free credits. No credit card required.