Ads

Boost published posts and run ads, audiences and lead forms on a connected network.

Ads run on an ad network connection, and this page covers Meta. Google Ads and TikTok Ads use these same endpoints through their own connections, each with a surface of its own: see Google Ads, LinkedIn Ads and TikTok Ads. GET /v1/ads/providers lists every network with a capabilities block; read that rather than branching on a network's name.

A Meta Ads connection is a Meta business or user login that reaches ad accounts and Facebook Pages, separate from the account used for posting. With one in place you can boost a post FoPost already published, create an ad in one call or build its campaign, ad set, creative and ad one at a time, manage everything already on the ad account, read insights, build audiences, run product catalogs, predict reach, search the public ad archive and collect leads from Instant Forms. These endpoints need the ads scope, and every one that can start or stop spend, or change what the public sees on an ad, also needs publish.

EndpointScopes
POST /v1/ads/boostads, publish
POST /v1/adsads, publish
PATCH /v1/ads/{id}ads, publish
DELETE /v1/ads/{id}ads, publish
POST, PATCH, DELETE and /duplicate on /v1/ads/campaigns, /v1/ads/ad-sets and /v1/ads/adsads, publish
POST /v1/ads/statusads, publish
POST, PATCH and DELETE under /v1/ads/catalogsads, publish
POST /v1/ads/reach-frequency/{id}/reserve and /cancelads, publish
POST, DELETE on /v1/ads/comments/{id} and its /reply and /hideads, publish
Everything else under /v1/adsads

Amounts are in the ad account's currency, in minor units: 1500 is $15.00 on a USD account.

Connect

GET /v1/ads/providers
GET /v1/ads/connections
POST /v1/ads/connections/{provider}/authorize
DELETE /v1/ads/connections/{id}?workspace_id=

/providers lists the networks this deployment knows, each with what it supports and whether it can be connected yet; configured: false means the network is not available and authorizing answers 503. {provider} is an id from that list, such as meta. Authorize returns the network's login URL for the workspaceId you pass. The user who calls it has to finish the login in their own browser session, because the callback checks that the same user came back. Removing a connection also deletes every ad record FoPost created through it.

GET /v1/ads/sources

lists each connection with its provider, the ad accounts (act_… on Meta) and the Pages its grant reaches. You need one of each to create an ad; ids are the network's own, and you pass them back as they came.

Boost a post

GET /v1/ads/boostable
POST /v1/ads/boost

/boostable lists published posts with a delivery on an account the connection reaches. Boost one:

curl -X POST https://api.fopost.com/v1/ads/boost \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
    "connectionId": "c4d5e6f7-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
    "adAccountId": "act_1234567890",
    "postId": "3a9f1d20-6c77-4b2e-9a01-8f5d2c3b4e10",
    "accountId": "b81e4f02-91a3-4c55-8de6-77a0c1f2d3e4",
    "name": "Autumn drop boost",
    "goal": "engagement",
    "budget": { "minor": 2000, "type": "daily" },
    "targeting": { "countries": ["US", "CA"], "ageMin": 21, "ageMax": 45, "gender": "all" }
  }'
FieldRequiredDescription
workspaceIdYesWorkspace the ad belongs to
connectionIdYesA Meta Ads connection in that workspace
adAccountIdYesact_…, from /sources
postId, accountIdYesThe post and the account it was delivered to, from /boostable
nameYesWhat the campaign is called on Meta
goalYesengagement, traffic, awareness, or video_views. A boost carries a post that already exists, so the goals that need their own destination are for standalone ads
budgetYesminor plus type of daily or lifetime, optional endAt
targetingYesSee below
pausedNoDefault true. Set false to start delivering at once

The response is the ad, 201. It starts paused unless you said otherwise, so a boost created from a script never spends until someone resumes it.

Create an ad

POST /v1/ads

Same fields as a boost, minus postId and accountId, plus the creative: pageId (from /sources), text (up to 125 characters), optional headline, destinationUrl, mediaUrl (a media library asset) and urlTags, a query string such as utm_source=meta&utm_medium=paid appended to every link in the ad.

This one call creates a campaign, an ad set, a creative and an ad on Meta. The next section builds the same four objects one at a time, and Meta receives exactly the same requests either way.

Goals

GET /v1/ads/goals?connection_id=

answers with the goals that connection can run right now. Ask rather than assume: a goal the deployment is not set up for is absent from that list and answers 400 unsupported if you send it anyway.

GoalWhat the ad doesAlso needs
engagementReactions, comments and shares on the ad—
trafficClicks to destinationUrldestinationUrl
awarenessReach, as cheaply as possible—
video_viewsPlays of the video in the ad—
messagesOpens a chat threadmessagingDestination
callsDials you from the adphoneNumber
whatsappOpens a WhatsApp threadAbsent unless the deployment has a WhatsApp business number
salesA catalog ad built from a product setproductSetId

messagingDestination is messenger, instagram_direct or whatsapp; the WhatsApp destination is refused for the same reason the whatsapp goal is hidden. phoneNumber is E.164, such as +14155550123. productSetId comes from Catalogs and turns the ad into a catalog ad: Meta fills the cards from the set, so text is the copy above them and headline and description may use {{product.name}} and {{product.description}}.

The same three fields work on POST /v1/ads/ad-sets, so a goal behaves identically whether you build the chain in one call or four.

The campaign tree

Everything on an ad account, whoever made it, is addressed by Meta's own ids through a connection. Nothing here is stored by FoPost: each call reads or writes Meta directly. An id on an ad account the connection does not reach answers 404, the same as an id that does not exist.

GET /v1/ads/accounts/{adAccountId}/tree?connection_id=

returns every campaign with its ad sets and each ad set's ads, with status, budget and objective.

Build an ad one object at a time:

POST /v1/ads/campaigns
POST /v1/ads/ad-sets
POST /v1/ads/creatives
POST /v1/ads/ads
RequestFields
CampaignworkspaceId, connectionId, adAccountId, name, goal, paused
Ad setworkspaceId, connectionId, campaignId, pageId, name, goal, budget, targeting, paused
AdworkspaceId, connectionId, adSetId, creativeId, name, paused

Each starts paused unless paused is false. Every object then has the same four operations, with ?workspace_id=&connection_id= on the path:

GET /v1/ads/campaigns/{id}
PATCH /v1/ads/campaigns/{id}
DELETE /v1/ads/campaigns/{id}
POST /v1/ads/campaigns/{id}/duplicate

and likewise under /v1/ads/ad-sets/{id} and /v1/ads/ads/{id}. PATCH changes name and status on all three, budgetMinor, endAt and targeting on an ad set, and creativeId on an ad. Deleting a campaign or ad set deletes everything under it. Duplicating copies everything under it too, paused unless the body says { "paused": false }.

Pause or resume up to 50 objects at once:

POST /v1/ads/status
{
  "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
  "connectionId": "c4d5e6f7-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "status": "paused",
  "objects": [
    { "id": "120210000000000001", "level": "campaign" },
    { "id": "120210000000000002", "level": "ad_set" }
  ]
}

Each object succeeds or fails on its own; the response lists ok and error for every one.

Creatives

GET /v1/ads/creatives?connection_id=&ad_account_id=
POST /v1/ads/creatives
GET /v1/ads/creatives/{id}?connection_id=
DELETE /v1/ads/creatives/{id}?workspace_id=&connection_id=

The list is the ad account's creative library. Create one with adAccountId, pageId, name, text and a format:

FormatMedia
imageOptional mediaUrl, a library image
videomediaUrl, a library video, plus an optional thumbnailMediaUrl. Without one, Meta picks a frame
carouselcards: 2 to 10, each with a library image mediaUrl and optional destinationUrl, headline and description
catalogNo media: productSetId instead, and Meta fills the cards from the set. See Product catalogs
partnershipNo media: creatorPostId, and creatorInstagramUserId for an Instagram post. See Partnership ads

Every format takes optional headline, destinationUrl, callToAction (LEARN_MORE by default, or SHOP_NOW, SIGN_UP, SUBSCRIBE, CONTACT_US, DOWNLOAD, GET_OFFER, BOOK_NOW, APPLY_NOW, WATCH_MORE) and urlTags. text is required except on partnership, where the creator's post carries its own copy. A creative runs nowhere until an ad uses it.

Insights

GET /v1/ads/insights?connection_id=&object_id=&since=&until=&breakdown=&daily=
GET /v1/ads/{id}/insights?workspace_id=&since=&until=&breakdown=&daily=

The first reads an ad account (act_…), campaign, ad set or ad; the second reads an ad created through FoPost. since and until are YYYY-MM-DD, inclusive, up to three years apart. The report has totals (impressions, reach, clicks, spendMinor, ctr as a percentage, and leads), a breakdown by age, gender, placement or country when you ask for one, and a day-by-day timeline when daily=true.

Reach estimate

POST /v1/ads/reach-estimate

Pass workspaceId, connectionId, adAccountId, pageId and a targeting block. The answer is Meta's estimated audience, lower to upper people; ready is false while Meta is still working it out.

Targeting

targeting needs at least one country or one location:

FieldDescription
countriesISO 3166-1 alpha-2 codes
ageMin, ageMax13 to 65
genderall, male, or female
locationsRegions, cities, postcodes or metro areas below country level
interests, behaviors, incomeMeta catalogue entries
facetsEntries for the facets a network defines for itself, keyed by search type
audienceIdsCustom audiences to include

Everything below country level comes from the catalogue:

GET /v1/ads/targeting/search?connection_id=&type=&q=

type is one of country, region, city, zip, metro, interest, behavior, or income on Meta. What a network accepts is its targetingFacets on GET /v1/ads/providers, so read that rather than hard-coding the list; a type the network does not serve answers 400 with error: "unsupported". Each result has an id and name you pass back as-is; a location also needs its type.

Manage

GET /v1/ads
POST /v1/ads/{id}/refresh?workspace_id=
PATCH /v1/ads/{id}?workspace_id=
DELETE /v1/ads/{id}?workspace_id=

GET /v1/ads lists the boosts and ads created through FoPost with the insights from their last refresh: impressions, reach, clicks and spendMinor. Refresh reads the current delivery status and lifetime insights from Meta. PATCH takes { "status": "active" } or { "status": "paused" }. Delete ends delivery and removes the ad on Meta as well as here.

GET /v1/ads/external

lists ads that live on the same ad accounts but were made elsewhere, read live from Meta and never stored.

Audiences

GET /v1/ads/audiences?connection_id=&ad_account_id=
POST /v1/ads/audiences

Create one by spec.subtype. Meta takes the first three; which subtypes a network takes follows its capabilities:

SubtypeFieldsWhat it is
CUSTOMemailsA customer list. Emails are hashed before they leave the API
LOOKALIKEoriginAudienceId, country, ratioPeople who resemble an existing audience
WEBSITEpixelId, retentionDays, urlContainsVisitors your pixel saw
COMPANY_LISTcompaniesA list of companies rather than people
ENGAGEMENTsource, sourceId, retentionDaysPeople who engaged with a page, ad, form or video

The list endpoint also returns the ad account's pixels.

GET /v1/ads/audiences/{id}?connection_id=
PATCH /v1/ads/audiences/{id}?workspace_id=&connection_id=
DELETE /v1/ads/audiences/{id}?workspace_id=&connection_id=
POST /v1/ads/audiences/{id}/users?workspace_id=&connection_id=

PATCH renames an audience or changes its description. /users adds more emails to a customer list after it was created; they are hashed before they leave the API. POST /v1/ads/audiences/{id}/companies does the same for a company list.

Lead forms

GET /v1/ads/lead-forms
POST /v1/ads/lead-forms
GET /v1/ads/lead-forms/{formId}/leads?connection_id=&page_id=&after=

Instant Forms belong to a Page. Create one with pageId, name, one to three questions from EMAIL, FULL_NAME and PHONE, a privacyPolicyUrl, a thankYouMessage, and an optional followUpUrl. Leads come back a page at a time, each with its fields; pass nextCursor as after for the next page.

GET /v1/ads/lead-forms/{formId}?connection_id=&page_id=
POST /v1/ads/lead-forms/{formId}/archive

The first returns one form with its questions, privacy policy and lead count. Archiving (body: workspaceId, connectionId, pageId) stops the form collecting; its leads stay readable.

The leads feed

POST /v1/ads/lead-pages
GET /v1/ads/lead-pages
DELETE /v1/ads/lead-pages/{pageId}?workspace_id=&connection_id=
GET /v1/ads/leads?form_id=&page_id=&cursor=&limit=

Subscribe a Page (body: workspaceId, connectionId, pageId) and every new lead on any of its forms arrives in the feed as it is submitted. Subscribing also backfills the most recent leads of each form. GET /v1/ads/leads lists leads across every subscribed Page in your workspaces, newest first, up to 100 at a time; pass nextCursor back as cursor. Unsubscribing stops new leads; the ones already in the feed stay. Removing the connection deletes its leads from the feed.

Product catalogs

A catalog holds your products; a product set is the slice of it one catalog ad runs from. Catalogs live on the connection's business portfolio, so a connection that reaches no portfolio cannot create one.

GET    /v1/ads/catalogs?connection_id=
POST   /v1/ads/catalogs
GET    /v1/ads/catalogs/{id}?connection_id=
PATCH  /v1/ads/catalogs/{id}
DELETE /v1/ads/catalogs/{id}?workspace_id=&connection_id=

Create one with workspaceId, connectionId, name and an optional vertical (commerce by default). Deleting a catalog deletes every product, feed and set in it.

Products

GET  /v1/ads/catalogs/{id}/products?connection_id=&after=
POST /v1/ads/catalogs/{id}/products

The list pages 50 at a time; pass nextCursor back as after. Write up to 500 products in one batch, keyed by your own retailerId:

curl -X POST https://api.fopost.com/v1/ads/catalogs/500123456789/products \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
    "connectionId": "c4d5e6f7-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
    "products": [
      {
        "op": "upsert",
        "retailerId": "SKU-1042",
        "name": "Trail Runner",
        "url": "https://yourbrand.com/shop/trail-runner",
        "imageUrl": "https://yourbrand.com/img/trail-runner.jpg",
        "priceMinor": 12900,
        "currency": "USD",
        "availability": "in stock"
      },
      { "op": "delete", "retailerId": "SKU-0007" }
    ]
  }'

Upserts and deletes travel in the same batch. priceMinor is minor units of currency. availability is in stock, out of stock, preorder, available for order or discontinued; condition is new, refurbished or used.

Feeds

A feed keeps the catalog in step with a product file you host.

GET    /v1/ads/catalogs/{id}/feeds?connection_id=
POST   /v1/ads/catalogs/{id}/feeds
DELETE /v1/ads/catalogs/{id}/feeds/{feedId}?workspace_id=&connection_id=
GET    /v1/ads/catalogs/{id}/feeds/{feedId}/uploads?connection_id=
POST   /v1/ads/catalogs/{id}/feeds/{feedId}/uploads

Create one with name, and url plus an optional schedule of HOURLY, DAILY or WEEKLY when Meta should fetch it for you. POST .../uploads fetches it now, optionally from a one-off url; GET .../uploads lists each run with its error and warning counts.

Product sets

GET    /v1/ads/catalogs/{id}/product-sets?connection_id=
POST   /v1/ads/catalogs/{id}/product-sets
PATCH  /v1/ads/catalogs/{id}/product-sets/{setId}
DELETE /v1/ads/catalogs/{id}/product-sets/{setId}?workspace_id=&connection_id=

A set takes a name and an optional filter, Meta's own product-set filter shape. Without one the set is the whole catalog.

Running a catalog ad

A catalog ad is the ordinary chain with a product set attached at two points:

{ "goal": "sales", "productSetId": "501234567890" }

on the campaign and ad set, then a creative with "format": "catalog" and the same productSetId, then an ad. Or send all of it to POST /v1/ads in one call. A product set on a catalog your connection does not reach answers 404, before anything is created on Meta.

Reach and frequency

A prediction prices a fixed flight before you buy it. Nothing is reserved until you say so, and a reservation holds inventory on the ad account.

GET  /v1/ads/reach-frequency?connection_id=&ad_account_id=
POST /v1/ads/reach-frequency
GET  /v1/ads/reach-frequency/{id}?connection_id=&ad_account_id=
POST /v1/ads/reach-frequency/{id}/reserve
POST /v1/ads/reach-frequency/{id}/cancel

Create one with adAccountId, name, targeting, placements, budgetMinor, startAt and endAt (ISO 8601), and an optional frequencyCap. The prediction carries reach, impressions, the budget it priced and reserved. Reserve and cancel take workspaceId, connectionId and adAccountId in the body and answer with the prediction as it now stands.

Ad Library

The public ad archive: ads anyone is running, not just yours.

GET /v1/ads/library?connection_id=&countries=&q=&page_ids=&active_status=&limit=&after=

countries is a comma-separated list of two-letter codes the ad reached and is required. Search by q (keyword or Page name) or by page_ids, up to ten. active_status is ACTIVE, INACTIVE or ALL.

Results are read from Meta on every request and stored nowhere, so what you get back is what the archive holds right now: an ad that stops running is simply absent from the next search. Each entry carries the Page, the creative bodies and titles, the platforms it runs on, its delivery dates and a snapshotUrl into the archive. The spend and impression ranges are only on the archive's disclosure entries; ordinary ads have none.

Partnership ads

A partnership ad runs a creator's own post as your ad, under the permission they granted you.

GET    /v1/ads/partnership/creators?connection_id=&page_id=
POST   /v1/ads/partnership/creators
DELETE /v1/ads/partnership/creators/{creatorId}?workspace_id=&connection_id=&page_id=

The list is the creators who allowlisted that Page. POST (body: workspaceId, connectionId, pageId, creatorId) asks a creator for permission and answers with the list as it now stands.

Once a creator has allowlisted you, promote one of their posts with a creative of "format": "partnership", creatorPostId (their Instagram media id, or their Page post as {page}_{post}) and, for an Instagram post, creatorInstagramUserId. The post carries its own copy, so text is not required.

Account settings

Everything the ad account itself holds, read through a connection and never stored.

GET /v1/ads/account/activity?connection_id=&ad_account_id=&since=&until=

is the change log: who changed what, and when. since and until are YYYY-MM-DD.

GET    /v1/ads/account/labels?connection_id=&ad_account_id=
POST   /v1/ads/account/labels
PATCH  /v1/ads/account/labels/{id}
DELETE /v1/ads/account/labels/{id}?workspace_id=&connection_id=&ad_account_id=
POST   /v1/ads/account/labels/{id}/apply

Labels group campaigns, ad sets and ads for reporting. apply takes objectId and a level of campaign, ad_set or ad, and keeps whatever labels the object already carries.

GET    /v1/ads/account/studies?connection_id=&ad_account_id=
POST   /v1/ads/account/studies
GET    /v1/ads/account/studies/{id}?connection_id=&ad_account_id=
DELETE /v1/ads/account/studies/{id}?workspace_id=&connection_id=&ad_account_id=

An A/B study splits traffic evenly across its cells, two to five of them, each with a name and the objectIds it tests, over startAt to endAt.

GET /v1/ads/account/ios-limits?connection_id=&ad_account_id=

is how many iOS 14 campaigns the account may run at once, per app.

GET    /v1/ads/account/high-demand-periods?connection_id=&ad_account_id=
POST   /v1/ads/account/high-demand-periods
DELETE /v1/ads/account/high-demand-periods/{id}?workspace_id=&connection_id=&ad_account_id=

A high-demand period tells Meta to expect heavier spend over a window, so pacing allows for it. Create one with startAt, endAt, budgetValue and a budgetValueType of ABSOLUTE or MULTIPLIER.

GET    /v1/ads/account/value-rule-sets?connection_id=&ad_account_id=
POST   /v1/ads/account/value-rule-sets
DELETE /v1/ads/account/value-rule-sets/{id}?workspace_id=&connection_id=&ad_account_id=

A value rule set weights conversions so some audiences count for more than others: a name and one to twenty rules, each a condition and a multiplier.

Comments on ads

Comments on a Facebook ad, including an ad made only for Ads Manager and never posted to the Page, arrive in the inbox with the Page's other comments, and you reply to or moderate them the same way.

Errors

A 422 means the network refused the request and message carries its reason, such as an ad account without a payment method or a Page the connection does not manage. A 403 with error: "no_ads_access" means the connection's grant does not reach ads on that account. A 400 with error: "unsupported" means the network cannot do what was asked at all.

Next

Related documentation
  • API Overview

    Base URL, envelopes, pagination, and errors for the FoPost REST API.

  • Authentication

    API keys, scopes, and workspace binding.

  • Publishing

    Create a post, target accounts, publish it, and read the per-account result.

  • Scheduling

    Schedule a post, repeat it, and import a batch from a spreadsheet.

  • Media

    Upload files, list the media library, and attach media to a post.

  • Validation

    Check content, text length, and media against platform rules before a post exists.

  • Accounts

    List connected social accounts, check their health, and refresh credentials.

  • Workspaces

    Workspaces, labels, and how isolation works across them.

Was this helpful?

On this page