Skip to main content
Socialift
Guide

Creating ad campaigns with the Meta Marketing API

The Marketing API builds an ad from four layers: a campaign (the objective, e.g. OUTCOME_TRAFFIC), an ad set (audience, budget, schedule), a creative (image, copy, link) and the ad itself. Budgets are integers in the currency's smallest unit, special_ad_categories is required on every campaign, and the safe pattern is to create everything PAUSED and activate after review.

Updated:

In short: how do you create a campaign via the API?

The order is fixed: pick an ad account (act_...), search and size your audience for free, then POST /act_{id}/campaigns (objective and special_ad_categories), /adsets (targeting, budget, optimization), /adimages and /adcreatives (image and copy), and finally /ads. Create each object PAUSED, and once reviewed switch all three levels to ACTIVE.

Permissions: ads_read for reporting, ads_management for creating and managing. Managing other businesses' ad accounts requires Advanced Access for ads_management — in other words, App Review.

The object hierarchy: what lives where

The object hierarchy: what lives where — Capability matrix by platform
LevelWhat it controlsEndpointSDK method
Ad accountCurrency, billing, accessact_{id}ads.ad_accounts
CampaignOne objective, special_ad_categories, optional shared budgetPOST /act_{id}/campaignscreate_campaign
Ad setAudience, budget, schedule, optimization goal and billing eventPOST /act_{id}/adsetscreate_adset
ImageThe hash a creative referencesPOST /act_{id}/adimagesupload_image
CreativeImage or video, copy, link, CTA; immutable once createdPOST /act_{id}/adcreativescreate_creative
AdTies an ad set to a creativePOST /act_{id}/adscreate_ad

All ads in one ad set share its targeting and optimization goal. Creatives can't be edited — create a new one and attach it to the ad.

Campaign objectives

Since Marketing API v17.0, new campaigns can only use the simplified OUTCOME objectives.

Campaign objectives — Capability matrix by platform
ObjectiveUse it for
OUTCOME_AWARENESSReaching as many people as possible, brand recall
OUTCOME_TRAFFICVisits to a website, app or profile
OUTCOME_ENGAGEMENTPost engagement, video views, messages
OUTCOME_LEADSLead forms and other inquiries
OUTCOME_SALESSales and conversions
OUTCOME_APP_PROMOTIONApp installs and in-app actions

Legacy objectives such as LINK_CLICKS, CONVERSIONS or REACH can't be used for new campaigns; existing campaigns created with them keep running.

What unit is the budget in?

daily_budget and lifetime_budget are integers in the ad account's currency, expressed in its smallest unit (Meta calls this the offset). For offset-100 currencies (USD, EUR, RUB and most others) a $20 daily budget is sent as 2000. For offset-1 currencies (JPY, KRW, IDR, VND, CLP, COP, CRC, HUF, ISK, PYG, TWD) you send the base-unit value.

Always compute the budget in the account's own currency (account.currency) — not every local currency is supported; for example, the Uzbek som (UZS) isn't on Meta's currency list.

A budget lives either on the campaign (Meta distributes it across ad sets) or on the ad set — never both. lifetime_budget requires an end_time; daily_budget is for ad sets running longer than 24 hours.

In the Socialift SDK, daily_budget_major=20 is multiplied by CURRENCY_OFFSET (100) and sent as 2000. That's correct for offset-100 currencies; if the account uses an offset-1 currency, divide your value by 100.

special_ad_categories is required on every campaign

Meta requires every new or edited campaign to declare whether it falls into a Special Ad Category. For regular ads you send an empty list ([]) — the SDK's create_campaign does this by default.

The categories are HOUSING, EMPLOYMENT, FINANCIAL_PRODUCTS_SERVICES (which replaced the old CREDIT value) and ISSUES_ELECTIONS_POLITICS. Once you pick a category, special_ad_category_country becomes required too.

Targeting is restricted in these categories: for housing, employment and financial ads aimed at the US, Canada or Europe, age is generally fixed at 18–65+, gender can't be chosen, location radius is at least 25 km (15 miles) and lookalike audiences are unavailable.

Launching a campaign from scratch in 8 steps

  1. Pick the ad account

    ads.ad_accounts returns the accounts the user can access, with their act_... ID and currency. Budgets are computed in that currency.

  2. Search interests and locations

    search_interests (type=adinterest) returns id, name, path and audience size bounds. search_locations (type=adgeolocation) returns the key used in targeting. Store IDs, not names — Meta may rename interests.

  3. Build the targeting spec

    At least one country is required unless you use a Custom Audience. age_min can't go below 13 and age_max can't exceed 65; genders is 1 for men, 2 for women, omitted for all. City radius is 17–80 km (10–50 miles).

  4. Estimate the audience without spending

    GET /act_{id}/delivery_estimate returns a monthly audience range (estimate_mau_lower_bound, estimate_mau_upper_bound) and estimate_dau. If estimate_ready is false, ask again shortly.

  5. Create the campaign

    Objective (OUTCOME_*), special_ad_categories and status=PAUSED.

  6. Create the ad set

    Targeting, budget, optimization_goal (e.g. LINK_CLICKS), billing_event (e.g. IMPRESSIONS) and PAUSED again.

  7. Image, creative and ad

    Upload the image to /adimages to get a hash, reference it inside the creative's object_story_spec (Page, link, copy, CTA), and pass {"creative_id": ...} when creating the ad.

  8. Review, then activate

    Check the preview, then set the campaign, ad set and ad to ACTIVE. Ads go through review first — watch effective_status for PENDING_REVIEW or DISAPPROVED.

Python: find and size the audience

Nothing gets created and nothing is spent — search and estimation only.

audience.py
import asyncio, os
from socialift import Ads

async def main() -> None:
    token = os.environ["META_USER_TOKEN"]
    async with Ads(app_secret=os.environ["FACEBOOK_APP_SECRET"]) as ads:
        account = (await ads.ad_accounts(token))[0]
        print(account.id, account.currency)

        interests = await ads.search_interests(token, "fitness", limit=5)
        cities = await ads.search_locations(token, "Austin", country_code="US")
        print([c["key"] for c in cities])  # for cities=[...]

        targeting = ads.build_targeting(
            countries=["US"],
            age_min=18,
            interests=[{"id": i["id"], "name": i["name"]} for i in interests[:3]],
        )  # advantage_audience=True by default
        estimate = await ads.delivery_estimate(token, ad_account_id=account.id, targeting=targeting)
        print(estimate.get("estimate_mau_lower_bound"), estimate.get("estimate_mau_upper_bound"))

asyncio.run(main())

Entries in interests are combined with OR. For "A AND B", use the flexible_spec parameter of build_targeting.

Meta never returns an exact number, only a range. If it's too narrow, drop some interests or keep Advantage+ audience enabled.

Python: campaign → ad set → creative → ad

In the SDK status defaults to PAUSED — no call starts spending on its own.

launch.py
from socialift import Ads

async def launch(ads: Ads, token: str, account_id: str, page_id: str, targeting: dict) -> str:
    campaign = await ads.create_campaign(
        token, ad_account_id=account_id, name="Fall sale",
        objective="OUTCOME_TRAFFIC", special_ad_categories=[],
    )
    adset = await ads.create_adset(
        token, ad_account_id=account_id, name="US 18+", campaign_id=campaign["id"],
        targeting=targeting, optimization_goal="LINK_CLICKS", billing_event="IMPRESSIONS",
        daily_budget_major=20,  # USD account: sent as 2000 cents
    )
    with open("banner.jpg", "rb") as f:
        image = await ads.upload_image(token, ad_account_id=account_id, image=f.read(), filename="banner.jpg")
    creative = await ads.create_creative(
        token, ad_account_id=account_id, name="Banner A", page_id=page_id,
        link="https://example.com", message="Fall deals", image_hash=image["hash"],
    )
    ad = await ads.create_ad(
        token, ad_account_id=account_id, name="Banner A",
        adset_id=adset["id"], creative_id=creative["id"],
    )
    return ad["id"]

For the ad to deliver, all three levels — campaign, ad set and ad — must be ACTIVE: if a parent is paused, the ad's effective_status reads CAMPAIGN_PAUSED or ADSET_PAUSED. Activate with await ads.set_status(token, node_id, "ACTIVE") for each level.

The most reliable kill switch is the campaign: set it to PAUSED and nothing beneath it delivers.

To run the ad under an Instagram account's identity, pass instagram_user_id to create_creative — Meta requires it in place of the deprecated instagram_actor_id.

Advantage+ audience must be explicit since v23.0

Advantage+ audience lets Meta's delivery system look beyond the audience you defined to find people likely to convert. In the API it's targeting.targeting_automation.advantage_audience: 1 on, 0 off.

Starting with Graph API v23.0, creating a new ad set either defaults this flag to 1 or requires an explicit 1 or 0. Updating existing ad sets isn't affected.

When it's on, age isn't a hard limit: age_min accepts only 18–25 and age_max is fixed at 65. A narrower age band goes in as a suggestion via age_range. If you need strict control (legal restrictions, for example), call build_targeting(..., advantage_audience=False) and pass age_min/age_max as hard bounds.

Permissions and Marketing API access tiers

ads_read covers ad reporting and the Insights API. ads_management reads and manages ad accounts and creates campaigns; it depends on pages_read_engagement and pages_show_list and requires Advanced Access for other businesses. Tech providers handling other businesses' data also need Access Verification.

The Marketing API has its own tiers: Limited Access (formerly Standard) comes automatically with the product and is heavily rate-limited per ad account — meant for development. Full Access (formerly Advanced) follows App Review and is lightly rate-limited. Meta's bar: at least 500 Marketing API calls in the last 15 days and an error rate under 15% across the last 500 calls.

Reading results with insights

Every level exposes /{object-id}/insights: ad account, campaign, ad set and ad.

report.py
summary = await ads.insights(token, campaign_id, since="2026-09-01", until="2026-09-16")
print(summary.spend, summary.impressions, summary.reach, summary.ctr)

rows = await ads.insights_rows(
    token, account_id, level="adset", date_preset="last_7d", breakdowns=["age", "gender"]
)
for row in rows:
    print(row.get("age"), row.get("gender"), row.get("spend"), row.get("cpc"))

Without dates, insights returns lifetime totals (date_preset=maximum); a bare request to Meta typically returns the last 30 days.

insights_rows returns raw broken-down rows — this is where you learn which age group clicks cheapest. Not every breakdown combination is allowed: if you get an error, add them one at a time.

Pre-launch checklist

  • Account currency and its offset are known; the budget is converted accordingly.
  • special_ad_categories is explicit ([] for regular ads).
  • Targeting includes at least one country or a Custom Audience.
  • The delivery_estimate range fits the goal.
  • Campaign, ad set and ad were created PAUSED and reviewed.
  • Launch = all three levels ACTIVE; stop = campaign PAUSED.
  • effective_status and spend are monitored regularly.

← Back to blog

Frequently asked questions

Why is my budget 100 times too big or too small?

The Marketing API expects budgets in the currency's smallest unit. In USD, $20 is 2000; in an offset-1 currency like JPY, 2000 means ¥2,000. Check the ad account currency and send the value accordingly.

Can I still use the LINK_CLICKS objective?

Not for new campaigns: since v17.0 only OUTCOME_* objectives are accepted. LINK_CLICKS now lives on as an optimization_goal at the ad set level.

Can I check audience size without spending money?

Yes. The delivery_estimate endpoint returns an estimated monthly audience range and daily active users. Meta gives ranges, never exact counts.

My ad was created but isn't delivering — why?

Usually the campaign or ad set is still PAUSED, or the ad is in review (PENDING_REVIEW) or rejected (DISAPPROVED). Check the ad's effective_status.

Does ads_management need App Review?

Not for your own ad accounts and app role users — Standard (Limited) Access covers that. Managing other businesses' ad accounts requires Advanced Access, meaning App Review and Business Verification.

Can I omit special_ad_categories?

No, it's required. Send an empty list for regular ads; for housing, employment, financial or political topics, set the matching category and special_ad_category_country.

Sources

  1. Meta: Ad Campaign reference
  2. Meta: Currencies
  3. Meta: Special Ad Categories
  4. Meta: Advantage+ audience
  5. Meta: Ad Account delivery estimate
  6. Meta: Marketing API authorization

Related pages