Ads

Meta Ads

Run Facebook and Instagram campaigns from the API, with conversation ads, creative testing, reach and frequency buys, previews and live insights.

Meta Ads covers Facebook, Instagram, Messenger, Threads, Audience Network and WhatsApp Status placements. It is the most complete network in Pozy: every operation in the ads overview works here, plus several that only Meta offers.

#Before you start

On Meta's side you need:

  • A Business portfolio with at least one ad account (act_<number>). You can also create one through the API.
  • A Facebook Page. Ads run as the Page. Instagram placements also need an Instagram professional account linked to that Page.
  • A pixel or dataset if the campaign optimizes for conversions. See Pixels and tags.
Instagram Login accounts cannot run ads

An Instagram account connected with Instagram Login never carries ads permissions. Ads calls made with its accountId return 422 with code linked_account_required. Connect a facebook or metaads account in the same profile and use that one.

#Connect

Call GET /v1/connect/facebook/ads (or /v1/connect/instagram/ads) with the profileId. Meta offers two login modes, picked with loginMode:

loginMode What it creates Use it when
classic (default) A metaads account linked to the profile's Facebook account. If that account already has the ads permissions, no consent screen appears. You already publish to the Page through Pozy.
business A standalone metaads account with a system-user token. No Facebook posting account is created. You only run ads, or you want a token that does not depend on one person staying an admin.

Either way, ads need a Page as the actor. With business login, pass pageId on the connect call or change the default Page later with PUT /v1/accounts/{accountId}/facebook-page.

A metaads account is for ads only. Page and Instagram insights need the organic Facebook or Instagram connection, which is a separate connected account.

#Create an ad account

POST /v1/ads/accounts creates a Meta ad account inside the customer's own Business portfolio. Find the portfolio with GET /v1/ads/businesses, then send accountId (an active metaads connection), businessId, name, currency and timezoneId. timezoneId is Meta's numeric timezone id, not an IANA name. The connection needs Business admin access.

Warning

A new ad account has no payment method. The customer adds one in Meta Ads Manager before anything delivers. The create is not idempotent: after a timeout, check the portfolio in Ads Manager before you try again.

#Create a campaign

POST /v1/ads/create builds a campaign, an ad set and an ad in one call. Every field sits at the top level, and the network is inferred from accountId.

curl -X POST https://api.pozy.ai/v1/ads/create \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: spring-sale-us-feed" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "act_1234567890",
    "name": "Spring sale - US",
    "goal": "traffic",
    "budgetAmount": 40,
    "budgetType": "daily",
    "status": "PAUSED",
    "headline": "30% off everything this week",
    "body": "Free shipping on orders over $50.",
    "imageUrl": "https://cdn.example.com/spring-1080.jpg",
    "linkUrl": "https://shop.example.com/spring",
    "callToAction": "SHOP_NOW",
    "countries": ["US"],
    "ageMin": 25,
    "ageMax": 54
  }'
Response (trimmed)
{
  "ad": {
    "_id": "66d4a1b2c3e4f5a6b7c8d9e2",
    "name": "Spring sale - US",
    "platform": "facebook",
    "status": "paused",
    "configuredStatus": "PAUSED",
    "reviewStatus": "in_review",
    "adType": "standalone",
    "goal": "traffic",
    "budget": { "amount": 40, "type": "daily" },
    "platformAdAccountId": "act_1234567890",
    "platformCampaignId": "120250000000000000",
    "platformAdSetId": "120250000000000001",
    "platformAdId": "120260000000000000",
    "creative": { "creativeId": "120250000000000005", "pageId": "811889972008357" }
  },
  "message": "Ad created"
}

Keep three ids from the response: _id for per-ad calls, platformCampaignId for campaign calls and platformAdSetId for ad set calls. creative.creativeId can be reused later as existingCreativeId.

Without status: "PAUSED" the ad is created active and goes straight into Meta's review. To provision a campaign container first and add ad sets to it later, use POST /v1/ads/campaigns, then pass the campaign id as existingCampaignId on each create. POST /v1/ads/campaigns creates the campaign paused unless you send status: "ACTIVE".

#Check a request without spending

Add validateOnly: true to run the whole tree through Meta's own validator. Nothing is created. You get 200 with one result per node (validated or skipped) and Meta's error verbatim when something is wrong. Meta can only validate an ad set against a real campaign, so on a fresh tree the ad set reports skipped unless you pass existingCampaignId. Dry runs work on the single-ad shape only, not with creatives[], adSetId or reserved buying.

#Goals

goal picks the Meta objective and a sensible optimization goal for it.

goal Meta objective Needs
traffic Traffic linkUrl
engagement Engagement
awareness Awareness
video_views Engagement, ThruPlay A video creative
lead_generation Leads, instant form leadGenFormId (Lead forms)
lead_conversion Leads, website promotedObject with a pixel
conversions Sales promotedObject.pixelId and customEventType
app_promotion App promotion promotedObject.applicationId and objectStoreUrl
catalog_sales Sales, catalog promotedObject.productCatalogId / productSetId (Catalogs)
page_likes, page_visits Engagement A Page

optimizationGoal, billingEvent and promotedObject override the defaults when you need Meta's exact vocabulary (for example LINK_CLICKS or OFFSITE_CONVERSIONS).

#Budgets and bidding

Field Values Notes
budgetAmount number Whole units of the ad account currency.
budgetType daily, lifetime A lifetime budget needs endDate.
budgetLevel adset (default), campaign campaign is Meta's Advantage campaign budget (CBO). Read budgetLevel on the tree before you change a budget, because the update goes to whichever level owns it.
startDate, endDate ISO 8601 A value without an offset is read in the ad account's timezone.
buyingType AUCTION (default), RESERVED RESERVED is a reach and frequency buy.

Bid strategies go inside platformSpecificData on create:

bidStrategy Companion field Behavior
LOWEST_COST_WITHOUT_CAP (default) Meta spends the budget for the most results.
LOWEST_COST_WITH_BID_CAP bidAmount Hard ceiling per auction.
COST_CAP bidAmount Target average cost per result.
LOWEST_COST_WITH_MIN_ROAS roasAverageFloor Value-optimized sales campaigns only. 2.0 means 2x ROAS.

platformSpecificData also takes dailyMinSpendTarget and lifetimeMinSpendTarget (reserve part of a campaign budget for one ad set), customerLifecycle (new customers only, or all) and adSchedule (dayparting rows of dayOfWeek, startHour, endHour, which need a lifetime budget).

Budget schedules raise a budget for a sale or a busy weekend: POST /v1/ads/high-demand-periods with exactly one of campaignId or adSetId, and GET /v1/ads/high-demand-periods to read them.

Value rule sets raise or lower the bid for segments worth more or less to you, without splitting them into separate ad sets. Create one with POST /v1/ads/value-rule-sets, then attach it with valueRuleSetId on the create or on PUT /v1/ads/ad-sets/{adSetId}. Meta allows 6 sets per ad account, 10 rules per set and 4 criteria per rule. An update replaces the whole set, so read it first and send every rule you want to keep.

#Creatives and formats

Exactly one media source per creative. Pick the shape:

Shape Fields Notes
Single image imageUrl JPEG or PNG, up to 30 MB.
Single video video: { url, thumbnailUrl? } or video: { id } MP4 or MOV up to 4 GB. The create waits for Meta to finish processing, up to 10 minutes, so set your HTTP timeout above that.
Carousel carouselCards (2 to 10) Each card has its own image and optional headline, description, link and button. optimizeCardOrder lets Meta reorder.
N ads in one ad set creatives[] See creative testing.
Asset pool dynamicCreative Up to several images, videos, bodies and titles that Meta recombines.
Per-placement assets placementAssets A 9:16 asset on Stories and Reels and a 1:1 on Feed, in one ad. All images or all videos. 1 to 10 rules.
Several languages translations + defaultLocale Up to 10 locales on one ad, so engagement stays on one post. Every locale needs its own headline, body and description.
Reuse a creative existingCreativeId From an earlier create or POST /v1/ads/creatives.

callToAction takes the values listed in the API reference, such as SHOP_NOW, LEARN_MORE, SIGN_UP and CONTACT_US. Media URLs must be public: Meta downloads them itself, and a hotlink-protected host fails.

#Advantage+ options

  • creativeFeatures opts a creative in or out of Meta's individual Advantage+ creative enhancements, keyed by Meta's feature names. Anything you leave out is opted out.
  • advantageAudience: 1 lets Meta expand delivery beyond your targeting. Pozy sends 0 for you unless you set it.
  • budgetLevel: "campaign" turns on Advantage campaign budget.
  • promotion attaches an offer (PERCENTAGE_OFF, AMOUNT_OFF, FREE_SHIPPING, FREE_RETURN, PROMO_CODE) to the creative. Check ad.creative.promotionStatus on a later read to see whether Meta kept it.

#Conversation and call ads

POST /v1/ads/messaging creates an ad that opens a chat instead of a web page. destination picks where:

destination Opens in Requires
whatsapp WhatsApp The Page paired with a verified WhatsApp Business number. whatsappPhoneNumber picks one of several.
messenger The Page's Messenger inbox A Page
instagram_direct Instagram Direct An Instagram professional account linked to the Page

The ad set is built with conversation optimization and the matching destination type. Useful extras:

  • welcomeMessage replaces Meta's greeting and pre-fills the person's first message.
  • workflowId starts one of your workflows in the conversation the click opens.
  • existingPostId or objectStoryId runs an organic post as the ad instead of a new creative.
  • adSetId adds creatives to a running conversation ad set without resetting its learning.

POST /v1/ads/call builds a call ad that dials phoneNumber (E.164). linkUrl must be a real web page, not a tel: link. POST /v1/ads/ctwa is the older click-to-WhatsApp endpoint; it still works, but new code should use /v1/ads/messaging with destination: "whatsapp".

#Creative testing

Send creatives[] instead of the single-creative fields to get one campaign, one ad set and N ads that share the budget. The ads are named <name> #1, <name> #2 and so on, and the response returns ads[] instead of ad. To add a variation later, call the create again with the ad set's id in adSetId and a single creative: it inherits the budget, targeting and schedule and keeps the learning Meta has done.

Rank variations with the per-ad metrics on GET /v1/ads/tree, or split one ad's results by the asset Meta served with the breakdowns image_asset, video_asset, body_asset and title_asset.

GET /v1/ads/studies lists the A/B tests and lift studies on the ad account.

#Partnership ads

Partnership ads run a creator's Instagram post under your ad account, with both handles on the ad. Ask a creator for permission with POST /v1/ads/partnership-permissions, track who has granted it with GET /v1/ads/partnership-permissions, and find their eligible posts with GET /v1/ads/partnership-content. This is in private beta, so the calls can return feature_not_available until it opens to your workspace.

#Reach and frequency

Reach and frequency buys lock a price for a fixed audience and frequency before you spend.

#Quote

POST /v1/ads/rf-predictions with the targeting, explicit placements, the window and either budgetAmount (Meta predicts reach) or reach (Meta predicts budget). frequencyCap is 1 to 90. Show the returned minimum and maximum budget and reach.

#Reserve

POST /v1/ads/rf-predictions/{predictionId}/reserve holds the inventory for about 14 days and returns a new prediction id. Release it with DELETE /v1/ads/rf-predictions/{predictionId}.

#Buy

POST /v1/ads/create with buyingType: "RESERVED" and rfPredictionId set to the reserved id. Send no budget or bid fields: they come from the reservation. Targeting and placements must match the quote.

#Previews

GET /v1/ads/{adId}/preview?formats=INSTAGRAM_STORY,MOBILE_FEED_STANDARD renders an existing ad. POST /v1/ads/preview renders a creative before it is on an ad, from existingCreativeId or a raw creativeSpec. Both return Meta's iframe HTML, one per format, up to 10 formats per call. Embed the snippet as it is and fetch it when the user opens the preview: the signed URL inside is short-lived.

#Ad comments

GET /v1/ads/{adId}/comments reads the comments on an ad, including dark posts that never appeared on the Page. An ad that runs on both Facebook and Instagram has two threads: pick one with placement=facebook or placement=instagram. Reply through the normal comments endpoints with the post id the response returns in meta.effectiveStoryId. Hiding and deleting through the ad endpoints is TikTok only and returns 501 on Meta.

#Manage running ads

Job Call
Pause or resume a campaign, ad set or ad PUT /v1/ads/campaigns/{campaignId}/status, PUT /v1/ads/ad-sets/{adSetId}/status, PUT /v1/ads/{adId}/status with status (active or paused) and platform
Pause or resume up to 50 campaigns POST /v1/ads/campaigns/bulk-status
Change an ad set budget, bid, schedule or optimization PUT /v1/ads/ad-sets/{adSetId}, delivery settings inside platformSpecificData
Change a campaign budget (CBO) PUT /v1/ads/campaigns/{campaignId} with budget
Change targeting PUT /v1/ads/{adId} with targeting (applies to the ad's ad set)
Swap the creative PUT /v1/ads/{adId} with creative. Meta creatives are immutable, so Pozy builds a new one and points the ad at it. The ad goes through review again.
Duplicate POST /v1/ads/campaigns/{campaignId}/duplicate, /ad-sets/{adSetId}/duplicate, /{adId}/duplicate
Delete DELETE on the campaign, ad set or ad. Deletes cascade down.

Each switch is independent. Pausing a campaign stops everything under it, but resuming it only restarts children whose own switch is on. When a status is already what you asked for, nothing is written and the response says skipped: 1.

Duplicates default to a deep copy created paused (statusOption: "PAUSED"), so nothing spends until you activate it. renameStrategy, renamePrefix and renameSuffix name the copies, and the response maps each source id to its copy in adSetIdMap and adIdMap. A large campaign takes minutes because Meta copies one object at a time.

GET /v1/ads/ad-sets/{adSetId} reads the ad set live from Meta, including learning_stage_info.status (LEARNING, SUCCESS, FAIL, WAIVING). A missing status means unknown, not "out of learning".

#Reporting

You want Call Cost to the account
Spend, CPC, CPM, conversions and ROAS per level GET /v1/ads/tree, GET /v1/ads, GET /v1/ads/{adId} None, served from synced metrics
A daily series GET /v1/ads/timeline None
One ad or campaign split by a dimension GET /v1/ads/{adId}/analytics, GET /v1/ads/campaigns/{campaignId}/analytics with breakdowns One Meta call per dimension
Any Meta field, breakdown or filter GET /v1/ads/insights with objectId, level, fields, breakdowns One live call
Long ranges or whole accounts POST /v1/ads/insights/reports, then poll GET /v1/ads/insights/reports/{reportRunId} A Meta job

Breakdown dimensions: age, gender, country, region, publisher_platform, platform_position, device_platform, impression_device, image_asset, video_asset, body_asset, title_asset.

Metrics on each node include spend, impressions, reach, clicks, ctr, cpc, cpm, conversions, actions, actionValues, purchaseValue and roas. Reach is de-duplicated within a day only, so never sum it across days. A freshly connected ad account can answer 202 with backfillPending: true while history loads: retry after the Retry-After delay.

When polling an async report, stop on Job Completed, Job Failed or Job Skipped. Start at 5 seconds and back off to a minute. More in Reporting.

#Regulated ads

  • specialAdCategories declares housing, employment, credit, financial products, politics or gambling ads. Meta then restricts targeting (no income or ZIP targeting).
  • Ads delivered in the EU need dsaBeneficiary and dsaPayor unless the ad account has defaults (GET /v1/ads/dsa-defaults).
  • Other regimes (Brazil, Thailand, India and more) use regionalRegulatedCategories and regionalRegulationIdentities.

#Limits and errors

Error Cause Fix
422 linked_account_required The account is an Instagram Login account Use a facebook or metaads account from the same profile.
400 with type: "platform_error" Meta refused the request. Meta's code and subcode are in platformError. Read Meta's message: it usually names the field.
400 naming two fields Two creative shapes that exclude each other Send one media source per creative.
409 BUDGET_LEVEL_MISMATCH You sent an ad set budget to a CBO campaign Update the budget on the campaign instead.
409 on a boost or create retry An identical request is still in flight Wait, then read GET /v1/ads. Send an Idempotency-Key.
502 video_thumbnail_unavailable Meta produced no poster for the video Send thumbnailUrl.

Locations overlap is the most common targeting error: do not send a city together with the country that contains it. See Targeting.

#Endpoints

Campaigns, ad sets, ads and reporting:

Endpoint What it does
POST /v1/ads/create Create standalone ad
POST /v1/ads/campaigns Create a standalone campaign
POST /v1/ads/boost Boost post as ad
GET /v1/ads/tree Get campaign tree
GET /v1/ads/campaigns List campaigns
GET /v1/ads/campaigns/{campaignId} Get live campaign details
PUT /v1/ads/campaigns/{campaignId} Update a campaign
PUT /v1/ads/campaigns/{campaignId}/status Pause or resume a campaign
POST /v1/ads/campaigns/bulk-status Pause or resume many campaigns
POST /v1/ads/campaigns/{campaignId}/duplicate Duplicate a campaign
DELETE /v1/ads/campaigns/{campaignId} Delete a campaign
GET /v1/ads/ad-sets List ad sets
GET /v1/ads/ad-sets/{adSetId} Get live ad-set details
PUT /v1/ads/ad-sets/{adSetId} Update an ad set
PUT /v1/ads/ad-sets/{adSetId}/status Pause or resume a single ad set
POST /v1/ads/ad-sets/{adSetId}/duplicate Duplicate an ad set
DELETE /v1/ads/ad-sets/{adSetId} Delete an ad set
GET /v1/ads/{adId} Get ad details
PUT /v1/ads/{adId} Update ad
PUT /v1/ads/{adId}/status Pause or resume a single ad
POST /v1/ads/{adId}/duplicate Duplicate an ad
DELETE /v1/ads/{adId} Cancel an ad
GET /v1/ads/{adId}/review Read the platform's review verdict for an ad
GET /v1/ads/{adId}/analytics Get ad analytics
GET /v1/ads/campaigns/{campaignId}/analytics Get campaign analytics
GET /v1/ads/timeline Get daily account metrics
GET /v1/ads/insights Flexible live insights query
POST /v1/ads/insights/reports Submit async insights report
GET /v1/ads/insights/reports/{reportRunId} Poll an async insights report run

Meta-only tools:

Endpoint What it does
GET /v1/ads/businesses Businesses list
GET /v1/ads/businesses/users Business users
POST /v1/ads/accounts Create ad account
PATCH /v1/ads/accounts Update ad account settings
GET /v1/ads/accounts/health Check an ad account before creating ads
GET /v1/ads/accounts/finance Ad account finances
GET /v1/ads/instagram-accounts List Instagram ad identities
GET /v1/ads/instagram-posts List Instagram posts to boost
GET /v1/ads/value-rule-sets List value rule sets
POST /v1/ads/value-rule-sets Create a value rule set
GET /v1/ads/value-rule-sets/{valueRuleSetId} Read a value rule set
PUT /v1/ads/value-rule-sets/{valueRuleSetId} Replace a value rule set
DELETE /v1/ads/value-rule-sets/{valueRuleSetId} Delete a value rule set
GET /v1/ads/studies A/B tests and lift studies
GET /v1/ads/activity Ad account change / audit log
GET /v1/ads/high-demand-periods List high-demand periods
POST /v1/ads/high-demand-periods Schedule a budget increase
GET /v1/ads/advertisable-applications List advertisable apps
GET /v1/ads/ios-fourteen-campaign-limits Get iOS 14 campaign limits
GET /v1/ads/page-users Page users of a business
POST /v1/ads/page-users Assign a user to a Page
DELETE /v1/ads/page-users Remove a user from a Page
GET /v1/ads/dsa-defaults Get ad account DSA defaults
GET /v1/ads/dsa-recommendations Get DSA recommendations
POST /v1/ads/preview Render pre-create ad previews
GET /v1/ads/{adId}/preview Render previews of an existing ad
GET /v1/ads/{adId}/media Direct video and image URLs for an ad
GET /v1/ads/creatives Creative library
POST /v1/ads/creatives Create a standalone creative
GET /v1/ads/images Ad image library
POST /v1/ads/images Upload an ad image from base64
GET /v1/ads/videos Ad video library
POST /v1/ads/videos Upload an ad video
GET /v1/ads/{adId}/comments List comments on an ad
GET /v1/ads/partnership-content List partnership ad content
GET /v1/ads/partnership-permissions List partnership permissions
POST /v1/ads/partnership-permissions Set partnership permission
GET /v1/ads/library Search the public Ad Library

Conversation ads and reach and frequency:

Endpoint What it does
POST /v1/ads/messaging Create messaging ad
POST /v1/ads/call Create Click-to-Call ad
POST /v1/ads/ctwa Create CTWA ad (deprecated)
POST /v1/ads/rf-predictions Create reach-frequency prediction
GET /v1/ads/rf-predictions/{predictionId} Get reach-frequency prediction
DELETE /v1/ads/rf-predictions/{predictionId} Cancel reach-frequency booking
POST /v1/ads/rf-predictions/{predictionId}/reserve Reserve reach-frequency inventory