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.
| Endpoint | Scopes |
|---|---|
POST /v1/ads/boost | ads, publish |
POST /v1/ads | ads, 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/ads | ads, publish |
POST /v1/ads/status | ads, publish |
POST, PATCH and DELETE under /v1/ads/catalogs | ads, publish |
POST /v1/ads/reach-frequency/{id}/reserve and /cancel | ads, publish |
POST, DELETE on /v1/ads/comments/{id} and its /reply and /hide | ads, publish |
Everything else under /v1/ads | ads |
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/sourceslists 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" }
}'| Field | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace the ad belongs to |
connectionId | Yes | A Meta Ads connection in that workspace |
adAccountId | Yes | act_…, from /sources |
postId, accountId | Yes | The post and the account it was delivered to, from /boostable |
name | Yes | What the campaign is called on Meta |
goal | Yes | engagement, 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 |
budget | Yes | minor plus type of daily or lifetime, optional endAt |
targeting | Yes | See below |
paused | No | Default 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/adsSame 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.
| Goal | What the ad does | Also needs |
|---|---|---|
engagement | Reactions, comments and shares on the ad | — |
traffic | Clicks to destinationUrl | destinationUrl |
awareness | Reach, as cheaply as possible | — |
video_views | Plays of the video in the ad | — |
messages | Opens a chat thread | messagingDestination |
calls | Dials you from the ad | phoneNumber |
whatsapp | Opens a WhatsApp thread | Absent unless the deployment has a WhatsApp business number |
sales | A catalog ad built from a product set | productSetId |
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| Request | Fields |
|---|---|
| Campaign | workspaceId, connectionId, adAccountId, name, goal, paused |
| Ad set | workspaceId, connectionId, campaignId, pageId, name, goal, budget, targeting, paused |
| Ad | workspaceId, 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}/duplicateand 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:
| Format | Media |
|---|---|
image | Optional mediaUrl, a library image |
video | mediaUrl, a library video, plus an optional thumbnailMediaUrl. Without one, Meta picks a frame |
carousel | cards: 2 to 10, each with a library image mediaUrl and optional destinationUrl, headline and description |
catalog | No media: productSetId instead, and Meta fills the cards from the set. See Product catalogs |
partnership | No 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-estimatePass 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:
| Field | Description |
|---|---|
countries | ISO 3166-1 alpha-2 codes |
ageMin, ageMax | 13 to 65 |
gender | all, male, or female |
locations | Regions, cities, postcodes or metro areas below country level |
interests, behaviors, income | Meta catalogue entries |
facets | Entries for the facets a network defines for itself, keyed by search type |
audienceIds | Custom 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/externallists 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/audiencesCreate one by spec.subtype. Meta takes the first three; which subtypes a network takes follows its capabilities:
| Subtype | Fields | What it is |
|---|---|---|
CUSTOM | emails | A customer list. Emails are hashed before they leave the API |
LOOKALIKE | originAudienceId, country, ratio | People who resemble an existing audience |
WEBSITE | pixelId, retentionDays, urlContains | Visitors your pixel saw |
COMPANY_LIST | companies | A list of companies rather than people |
ENGAGEMENT | source, sourceId, retentionDays | People 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}/archiveThe 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}/productsThe 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}/uploadsCreate 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}/cancelCreate 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}/applyLabels 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.