Creating Facebook ads through the Marketing API means building four objects in order: a campaign, an ad set, an ad creative, and an ad that ties the last two together. Each one is a POST to an edge under your ad account node, and each one fails loudly if a required field is missing. The current release is v26.0, published on July 29, 2026, and every path below is versioned against it.
/v26.0/act_<AD_ACCOUNT_ID>/campaigns with name, objective, status and special_ad_categories, then to /adsets with a budget, billing_event, optimization_goal and targeting, then to /adcreatives, then to /ads with the ad set ID and creative ID. You need the ads_management permission with Advanced Access, which requires App Review and business verification, and you should create everything with status=PAUSED until you have reviewed it.The API is unforgiving in a useful way: it validates the whole object graph server side, so a campaign built correctly through the API behaves identically to one built in Ads Manager. What trips teams up is not the object model, it is the access tiers, the rate limit accounting, and the fact that budgets are expressed in the minor unit of the account currency.
Access levels, permissions and App Review
A new app gets Standard Access to the ads permissions, which sounds generous and is not. Standard Access works only against ad accounts and Pages where the person authorizing the token holds a role on your app, or where the asset is owned by your own business. The moment you want to manage a customer’s ad account, you need Advanced Access.
| Permission | What it unlocks | Needs App Review |
|---|---|---|
ads_read | Reading campaigns, ad sets, ads and insights | Yes, for Advanced Access |
ads_management | Creating and editing ad objects, audiences, creatives | Yes, for Advanced Access |
business_management | Reading and managing Business Manager assets | Yes |
pages_show_list | Listing Pages the user administers | No |
Advanced Access to Ads Management also requires business verification of the legal entity behind the app. Budget real time for it. The review itself wants a screen recording that shows a person authorizing your app, the permission dialog, and your product then doing something with the granted data. A slide deck will be rejected.
For production automation, stop using a personal user token and create a system user inside Business Manager instead. System user tokens issued with Standard Access to the Marketing API do not carry an expiry time, which removes the single most common cause of a pipeline breaking at 3am. The token mechanics are covered in detail in our guide to Facebook API authentication and access tokens.
The object model
Four nodes, one hierarchy. The campaign holds the objective and any campaign level budget. The ad set holds the money, the schedule, the audience and the optimization goal. The creative holds the actual content. The ad joins a creative to an ad set.
Ad Account act_<AD_ACCOUNT_ID>
└── Campaign objective, special_ad_categories, budget optimization
└── Ad Set budget, schedule, targeting, optimization_goal, billing_event
└── Ad links one ad set to one ad creative
└── Ad Creative page, link, copy, image or videoThe objective values are the outcome based set that replaced the older objective names: OUTCOME_AWARENESS, OUTCOME_TRAFFIC, OUTCOME_ENGAGEMENT, OUTCOME_LEADS, OUTCOME_APP_PROMOTION and OUTCOME_SALES. Pick the one that matches the conversion you actually care about, because it constrains which optimization goals the ad set will accept.
Creating a campaign, ad set and ad with curl
Start with the campaign. special_ad_categories is required even when nothing applies, in which case you send an empty array.
curl -X POST \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/campaigns" \
-F "name=Fall Traffic Push" \
-F "objective=OUTCOME_TRAFFIC" \
-F "status=PAUSED" \
-F "special_ad_categories=[]" \
-F "access_token=<YOUR_ACCESS_TOKEN>"The response is a JSON object containing the new campaign ID. Feed that into the ad set. Budgets are integers in the minor unit of the account currency, so 2500 on a USD account is twenty five dollars, not twenty five hundred.
curl -X POST \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/adsets" \
-F "name=US Adults 25 to 54" \
-F "campaign_id=<CAMPAIGN_ID>" \
-F "daily_budget=2500" \
-F "billing_event=IMPRESSIONS" \
-F "optimization_goal=LINK_CLICKS" \
-F "bid_strategy=LOWEST_COST_WITHOUT_CAP" \
-F "targeting={\"geo_locations\":{\"countries\":[\"US\"]},\"age_min\":25,\"age_max\":54}" \
-F "status=PAUSED" \
-F "access_token=<YOUR_ACCESS_TOKEN>"Then the creative, and finally the ad that references both IDs.
curl -X POST \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/ads" \
-F "name=Fall Traffic Ad 01" \
-F "adset_id=<AD_SET_ID>" \
-F "creative={\"creative_id\":\"<CREATIVE_ID>\"}" \
-F "status=PAUSED" \
-F "access_token=<YOUR_ACCESS_TOKEN>"The Business SDK in Python
Hand rolling requests works, but the Business SDK saves you from string escaping the targeting spec and gives you typed field constants. Install it from PyPI.
pip install facebook_businessimport os
from facebook_business.api import FacebookAdsApi
from facebook_business.adobjects.adaccount import AdAccount
from facebook_business.adobjects.campaign import Campaign
FacebookAdsApi.init(
app_id=os.environ["FB_APP_ID"],
app_secret=os.environ["FB_APP_SECRET"],
access_token=os.environ["FB_ACCESS_TOKEN"],
api_version="v26.0",
)
account = AdAccount("act_" + os.environ["FB_AD_ACCOUNT_ID"])
campaign = account.create_campaign(params={
Campaign.Field.name: "Fall Traffic Push",
Campaign.Field.objective: "OUTCOME_TRAFFIC",
Campaign.Field.status: Campaign.Status.paused,
Campaign.Field.special_ad_categories: [],
})
print(campaign["id"])Keep the app secret and the token in environment variables or a secret manager. Never commit them, and never ship an app secret inside a mobile binary. If your product also has a mobile client, the split between what belongs on the device and what belongs on your server is spelled out in our walkthrough of using the Facebook SDKs in mobile app development.
Rate limits and how to read the headers
Marketing API throttling is scored per ad account. A read call costs one point and a write call costs three. Meta documents a maximum score of 60 on the development tier and 9000 on the standard tier, with a decay window of 300 seconds. The standard tier also blocks for only 60 seconds when you cross the line, which is the practical reason to care about upgrading. A separate limit of 100 queries per second applies to mutations on campaigns, ad sets and ads.
Two response headers tell you where you stand. X-Ad-Account-Usage carries acc_id_util_pct, reset_time_duration and ads_api_access_tier. X-Business-Use-Case-Usage carries call_count, total_cputime, total_time and estimated_time_to_regain_access.
curl -sD - -o /dev/null \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/campaigns?fields=name,status&access_token=<YOUR_ACCESS_TOKEN>" \
| grep -i "usage"estimated_time_to_regain_access and back off for at least that long. Tight retry loops are the fastest way to keep an account blocked for the rest of the hour.Pulling insights without melting your quota
Reporting lives on an /insights edge that hangs off the ad account, campaign, ad set or ad node. Small queries can run synchronously.
curl -G \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/insights" \
-d "level=campaign" \
-d "fields=campaign_name,impressions,clicks,spend,cpm,ctr" \
-d "date_preset=last_7d" \
-d "access_token=<YOUR_ACCESS_TOKEN>"Anything large should go asynchronous. POST to the same edge, keep the returned report_run_id, poll that node until async_status reads Job Completed and async_percent_completion reaches 100, then read the results from /<REPORT_RUN_ID>/insights. Meta notes that an asynchronous request can take up to an hour including retries, and that the run ID expires after 30 days.
curl -X POST \
"https://graph.facebook.com/v26.0/act_<AD_ACCOUNT_ID>/insights" \
-F "level=ad" \
-F "fields=ad_name,impressions,spend,actions" \
-F "time_range={\"since\":\"2026-08-01\",\"until\":\"2026-08-31\"}" \
-F "breakdowns=publisher_platform" \
-F "access_token=<YOUR_ACCESS_TOKEN>"Spread report jobs through the day instead of firing them all at midnight, filter down to the objects you care about, and split long date ranges into chunks. The same reporting fields are what you would see in the interface, and our piece on retrieving insights and analytics data maps the API names to the columns marketers recognize.
Troubleshooting
Error 200 with a permissions message. The token holder does not have the right role on the ad account, or your app is still on Standard Access and the account is not one you own. Check both before you assume the permission itself is missing.
Invalid parameter on adset creation. Nine times out of ten the targeting spec is a string that was not valid JSON after shell escaping, or the optimization goal is not compatible with the campaign objective. Post the targeting spec through a file with -F "targeting=<spec.json" to sidestep quoting problems.
Budget rejected as below the minimum. Minimums are enforced per currency and per billing event. Remember the value is in minor units, so a daily budget of 100 is one dollar and will be refused.
Insights returns fewer rows than Ads Manager. The default attribution setting and the default time zone are account level properties. Set action_attribution_windows explicitly and confirm the account time zone before you accuse the API of losing data.
A version error appears after a quiet weekend. A version you pinned reached its expiration date and calls now resolve against a newer default. Read the Graph API changelog and move the pin deliberately rather than dropping the version segment from the URL.
Frequently asked questions
Can I use the Marketing API without App Review?
Yes, in a limited way. Standard Access lets you manage ad accounts owned by your own business and accounts where the authorizing person holds a role on your app. That is enough to build and test. Managing a client’s account in production requires Advanced Access and business verification.
Which token type should a scheduled job use?
A system user token issued from Business Manager. User tokens expire and get invalidated when the person changes their password or revokes the app. System user tokens with Standard Access to the Marketing API carry no expiry time, which is what a nightly sync needs.
How do I upload an image for a creative?
POST the file to the /act_<AD_ACCOUNT_ID>/adimages edge first. The response returns a hash, and you reference that hash in the creative’s object story spec. Videos follow the same pattern through the /advideos edge and need a moment to finish processing.
What is the difference between a campaign objective and an optimization goal?
The objective sits on the campaign and describes the business outcome. The optimization goal sits on the ad set and tells delivery which event to chase. The API only accepts combinations that make sense, so an objective mistake surfaces as an ad set validation error.
Should I build in the API or in Ads Manager?
Use the interface for one off campaigns and creative experiments. Use the API when you are generating campaigns from a product feed, syncing budgets from a finance system, or managing more accounts than a person can click through. Our guide to setting up a Facebook ad campaign covers the manual path.
The bottom line
The Marketing API is a straightforward CRUD surface once you accept its two rules: build objects from the top down, and treat the ad account as the unit that gets rate limited. Pin your calls to v26.0, create everything paused, and read the usage headers on every response so your backoff logic has real numbers to work with.
The work that actually takes time is upstream of the code. Get Advanced Access and business verification started early, move automation onto a system user token, and put your app secret somewhere a repository scan will never find it. Do that and the rest is just JSON.
About this article: GeekBlog covers U.S. technology news, AI, phones, smartwatches and gaming. Every story is written and checked under our Editorial Policy. Spotted a mistake or have a story tip? Contact our editors.

