Meta Ads
Your agents build Facebook and Instagram campaigns on your Meta ad account, move budget to the winning ad, and stop spend by your rule.
- Connect
- Sign in once, 4 grants
- Runs through
- Your account
- First run
- Read the ads your rivals have run longest, about 0.2¢
Build a $400 Meta message test with two headlines for property managers, and show me what Meta would accept.
curl -X POST "https: //api.sb/ leaseline.app/ campaigns/ create? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"venue": "facebook", "objective": "traffic", "budget": {"amount": 400, "currency": "USD", "per": "test"}, "ads": ["ads/ rent-spreadsheet-a", "ads/ rent-spreadsheet-b"], "stopRule": "pause the losing headline at 7, 700 impressions each"}'
npx startup-builder do Campaign.create --try --data '{"venue": "facebook", "objective": "traffic", "budget": {"amount": 400, "currency": "USD", "per": "test"}, "ads": ["ads/ rent-spreadsheet-a", "ads/ rent-spreadsheet-b"], "stopRule": "pause the losing headline at 7, 700 impressions each"}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Campaign.create({ venue: 'facebook', objective: 'traffic', budget: { amount: 400, currency: 'USD', per: 'test' }, ads: ['ads/ rent-spreadsheet-a', 'ads/ rent-spreadsheet-b'], stopRule: 'pause the losing headline at 7, 700 impressions each' })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Campaign.create({ venue: 'facebook', objective: 'traffic', budget: { amount: 400, currency: 'USD', per: 'test' }, ads: ['ads/ rent-spreadsheet-a', 'ads/ rent-spreadsheet-b'], stopRule: 'pause the losing headline at 7, 700 impressions each' })
{
"$type": "Campaign",
"mode": "try",
"stored": false,
"venue": "facebook",
"placements": [
"facebook",
"instagram"
],
"objective": "OUTCOME_TRAFFIC",
"optimizeFor": "landing-page views",
"budget": {
"amount": 400,
"currency": "USD"
},
"validation": {
"validateOnly": true,
"success": true,
"rejected": []
},
"estimate": {
"impressionsEach": 12000,
"readAt": 7700
},
"stopRule": "pause the losing headline at 7, 700 impressions each"
}
Connect once
- Call
Integration.connectwithsystem:and'ads' venue:, then open the'facebook' connectUrlit returns and sign in as yourself with Meta. - Pick the Business, the ad account, the page the ads run as, and the pixel on your site.
- Set the spend on your standing approval with
Mandate.authorize: the most your agents may spend a day and in all; Meta bills your ad account, never the platform.
| Grant | What it unlocks | Scope |
|---|---|---|
| Build and change ads | Campaigns, ad sets, creatives and ads, and pausing or rebudgeting them | ads_management |
| Read results | Spend, reach, clicks and results by campaign, ad set, ad, day and placement | ads_read |
| Business assets | The ad account, page and pixel your Business holds | business_management |
| Lead-ad leads | Each lead-form entry the moment it is submitted | leads_retrieval |
| To | Do this |
|---|---|
| Pause | Integration.pause stops every call through Meta at once and keeps the token; Integration.resume picks up from the same cursor. |
| Reconnect | Integration.reconnect returns a fresh connectUrl that lasts one hour, and keeps the Integration's id and cursor, so nothing imports twice. |
| Disconnect | Integration.revoke revokes the token with Meta; every record it brought in stays in your records. |
What your agents do
| Ask | Verb | Runs through | Price |
|---|---|---|---|
| Turn my approved list into a custom audience and a 1% lookalike. | Audience.create | Your account | Free |
| Build a $400 message test: one campaign, one ad set, two headlines. | Campaign.create | Your account | Free |
| Add a third ad with the winning headline and the new image. | Ad.create | Your account | Free |
| Move the budget to the winner and pause the other. | Campaign.update | Your account | Free |
| Show me spend, clicks and cost per signup by ad for the last 7 days. | Metric.query | Your account | Free |
| Qualify each lead-ad entry as it arrives. | Lead.qualify | Your account | Free |
Calls on your own account are free; Meta bills your spend directly.
Reading this site’s public data needs no account: Ad.find is a Data call (the three ways a call runs).
What it holds
| Noun | The account’s object | When | Which side leads |
|---|---|---|---|
| Campaign | campaign | Your agent builds it | Your records |
| Ad | adset, | Your agent builds it; Meta’s review sets its status | Your records |
| Audience | customaudience | Your agent matches a list | Your records |
| Lead | leadgen entry | Someone submits a lead form | The account |
| Entry | insights: | Each day’s spend settles | The account |
Events and webhooks
| Direction | Event |
|---|---|
| Emits | Campaign.created, Campaign.updated, Ad.created, Audience.created, Lead.created, Entry.posted |
Hears leadgen | Becomes Lead.created |
Hears ad review status | Becomes Ad.updated |
Any Event can wake a Workflow, and a Watch delivers it to your endpoint, signed (Events and webhooks).
Limits
| What | Limit | Call |
|---|---|---|
| Minimum daily budget | Meta sets it per ad account, currency and what the ad set pays for; the agent reads it from your ad account before it builds, and a budget under it refuses with venue-rejected | Campaign.create |
| Ad review | Meta reviews every new ad before it runs; a disapproval comes back as venue-rejected with Meta’s reason | Ad.create |
| Spend | Your standing approval holds the daily and total cap; a change past it waits with over-mandate | Campaign.update |
| One ad set | A test under $1,000 runs in one ad set, never split, so every result feeds the same learning | Campaign.create |
Start here
| Move | The rule |
|---|---|
| Structure | One campaign on the traffic objective, one ad set with a broad audience optimizing for landing-page views, and two ads that differ in one phrase of the headline. |
| Budget | $300 to $500 for the message test. Optimize for signups only when a week’s budget reaches 50 times your cost per signup: at $25 a signup, $1,250 a week for one ad set. |
| Creative | A 4:5 feed image at 1440 × 1800, primary text of 50 to 150 characters and a headline under 27; both ads share the image and differ in one headline phrase. |
| Kill rule | Read at about 7,700 impressions a headline: keep the one ahead by 50% or more and pause the other. With no winner at $500, stop and rewrite the angle before you touch the image. |
| Benchmark | Source |
|---|---|
| An ad set leaves learning at about 50 optimization events in 7 days | Practitioner reports, consistent across sources |
| Two headlines at 1% CTR need about 7,700 impressions each to show a 50% lift | Our calculation: 80% power, 5% two-sided |
| $400 at about $16 per 1,000 impressions buys 12,000 impressions for each of 2 headlines | Our planning number |
Field map
| Our field | Meta Ads’s field | Notes | Calls |
|---|---|---|---|
Campaign.objective | campaign.objective | leads is OUTCOME_LEADS, traffic OUTCOME_TRAFFIC, conversions OUTCOME_SALES | Campaign.create, Campaign.update, Ad.create |
Campaign.budget | adset.daily_budget or adset.lifetime_budget | In the account currency’s smallest unit: $40.00 is 4000 | Campaign.create, Campaign.update, Ad.create |
Campaign.startsAt | adset.start_time | None. | Campaign.create |
Campaign.endsAt | adset.end_time | None. | Campaign.create |
Campaign.state | campaign.status | live is ACTIVE, paused is PAUSED | Campaign.update |
Ad.body | object_story_spec.link_data.message | The primary text | Ad.create |
Ad.headline | object_story_spec.link_data.name | None. | Ad.create |
Ad.landingUrl | object_story_spec.link_data.link | None. | Ad.create |
Audience.query | customaudience (subtype CUSTOM) | Emails are hashed before they leave your records | Audience.create |
Audience.lookalike | lookalike_spec.ratio | 1 is 0.01 | Audience.create |
Lead.interest | leadgen.field_data | None. | Lead.qualify |
What try does here
Sends the whole build with Meta’s validate_only execution option: Meta runs every check and creates nothing, and the response names any field it would reject.
How long a connection lasts
A connection lasts about 60 days, the life of Meta’s long-lived sign-in; you sign in with Meta again to start the next 60.
The platform asks you to reconnect 7 days before it runs out; until you do, a call that needs it refuses with needs-reauth.
Formats
| Site | Format | Size | Limits |
|---|---|---|---|
| Feed image ad | 1440 × 1800, 4:5 | Primary text 50 to 150 characters; headline 27; up to 30 megabytes | |
| Feed image | 320 to 1,440 pixels wide, 4:5 to 1.91:1 | JPEG only, up to 8 megabytes; caption 2,200 characters, 30 hashtags, 20 mentions | |
| Carousel | None. | Up to 20 images or videos, cropped to the first item’s ratio; counts as one post | |
| Reel | 9:16 | MOV or MP4, 3 seconds to 15 minutes, up to 300 megabytes, 23 to 60 frames a second | |
| Story | 9:16 | Image up to 8 megabytes; video 3 to 60 seconds, up to 100 megabytes |
Plays and strategies
| Kind | Where |
|---|---|
| Plays | Paid social, Positioning tournament, Retargeting, Promote winners, Creator ads, Instant response |
| Strategies | Paid |
Used by
- Workflows: Ad test setup
- Agent tasks: Ad kit · Message test
Reference
6 calls, each with its path and price
| Call | Path | Price |
|---|---|---|
Audience.create | POST / | Free |
Campaign.create | POST / | Free |
Ad.create | POST / | Free |
Campaign.update | POST / | Free |
Metric.query | POST / | Free |
Lead.qualify | POST / | Free |