URL: /code/integrations/meta-ads · Id: `page:/code/integrations/meta-ads`

# 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.

Your agents build Facebook and Instagram campaigns on your Meta ad account, move budget to the winning ad, and stop spend by your rule.

```text title="Run it · Ask"
Build a $400 Meta message test with two headlines for property managers, and show me what Meta would accept.
```

```ts title="Run it · SDK"
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' })
```

```json title="Run it · Response"
{
  "$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:** Sign in once, 4 grants
- **Runs through:** [Your account](/code/integrations#three-ways)
- **First run:** Read the ads your rivals have run longest, about 0.2¢
- **Sites:** [Facebook](/venues#facebook), [Instagram](/venues#instagram)

## Connect once
1. Call `Integration.connect` with `system: 'ads'` and `venue: 'facebook'`, then open the `connectUrl` it returns and sign in as yourself with Meta.
2. Pick the Business, the ad account, the page the ads run as, and the pixel on your site.
3. 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`](/code/apps/ads/create-audience) | Your account | Free |
| Build a $400 message test: one campaign, one ad set, two headlines. | [`Campaign.create`](/code/apps/ads/create-campaign) | Your account | Free |
| Add a third ad with the winning headline and the new image. | [`Ad.create`](/code/apps/ads/create-ad) | Your account | Free |
| Move the budget to the winner and pause the other. | [`Campaign.update`](/code/apps/ads/update-campaign) | Your account | Free |
| Show me spend, clicks and cost per signup by ad for the last 7 days. | [`Metric.query`](/code/apps/analytics-and-experiments/query) | Your account | Free |
| Qualify each lead-ad entry as it arrives. | [`Lead.qualify`](/code/apps/crm/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`](/code/data/ads/find) is a Data call (the [three ways a call runs](/code/integrations#three-ways)).

## What it holds
| Noun | The account’s object | When | Which side leads |
| --- | --- | --- | --- |
| [Campaign](https://startupbuilder.dev/business/nouns/campaign) | `campaign` | Your agent builds it | Your records |
| [Ad](https://startupbuilder.dev/business/nouns/ad) | `adset, ad and adcreative` | Your agent builds it; Meta’s review sets its status | Your records |
| [Audience](https://startupbuilder.dev/business/nouns/audience) | `customaudience` | Your agent matches a list | Your records |
| [Lead](https://startupbuilder.dev/business/nouns/lead) | `leadgen entry` | Someone submits a lead form | The account |
| [Entry](https://startupbuilder.dev/business/nouns/entry) | `insights: spend` | 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](/code/events)).

## 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`](/code/apps/ads/create-campaign) |
| Ad review | Meta reviews every new ad before it runs; a disapproval comes back as `venue-rejected` with Meta’s reason | [`Ad.create`](/code/apps/ads/create-ad) |
| Spend | Your standing approval holds the daily and total cap; a change past it waits with `over-mandate` | [`Campaign.update`](/code/apps/ads/update-campaign) |
| One ad set | A test under $1,000 runs in one ad set, never split, so every result feeds the same learning | [`Campaign.create`](/code/apps/ads/create-campaign) |

## 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`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign), [`Ad.create`](/code/apps/ads/create-ad) |
| `Campaign.budget` | `adset.daily_budget or adset.lifetime_budget` | In the account currency’s smallest unit: $40.00 is 4000 | [`Campaign.create`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign), [`Ad.create`](/code/apps/ads/create-ad) |
| `Campaign.startsAt` | `adset.start_time` | None. | [`Campaign.create`](/code/apps/ads/create-campaign) |
| `Campaign.endsAt` | `adset.end_time` | None. | [`Campaign.create`](/code/apps/ads/create-campaign) |
| `Campaign.state` | `campaign.status` | `live` is ACTIVE, `paused` is PAUSED | [`Campaign.update`](/code/apps/ads/update-campaign) |
| `Ad.body` | `object_story_spec.link_data.message` | The primary text | [`Ad.create`](/code/apps/ads/create-ad) |
| `Ad.headline` | `object_story_spec.link_data.name` | None. | [`Ad.create`](/code/apps/ads/create-ad) |
| `Ad.landingUrl` | `object_story_spec.link_data.link` | None. | [`Ad.create`](/code/apps/ads/create-ad) |
| `Audience.query` | `customaudience (subtype CUSTOM)` | Emails are hashed before they leave your records | [`Audience.create`](/code/apps/ads/create-audience) |
| `Audience.lookalike` | `lookalike_spec.ratio` | 1 is 0.01 | [`Audience.create`](/code/apps/ads/create-audience) |
| `Lead.interest` | `leadgen.field_data` | None. | [`Lead.qualify`](/code/apps/crm/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`](/code/refusals#needs-reauth).

## Formats
| Site | Format | Size | Limits |
| --- | --- | --- | --- |
| [Facebook](/venues#facebook) | Feed image ad | 1440 × 1800, 4:5 | Primary text 50 to 150 characters; headline 27; up to 30 megabytes |
| [Instagram](/venues#instagram) | 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 |
| [Instagram](/venues#instagram) | Carousel | None. | Up to 20 images or videos, cropped to the first item’s ratio; counts as one post |
| [Instagram](/venues#instagram) | Reel | 9:16 | MOV or MP4, 3 seconds to 15 minutes, up to 300 megabytes, 23 to 60 frames a second |
| [Instagram](/venues#instagram) | 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](/plays/paid-social), [Positioning tournament](/plays/positioning-tournament), [Retargeting](/plays/retargeting), [Promote winners](/plays/promote-winners), [Creator ads](/plays/creator-ads), [Instant response](/plays/instant-response) |
| Strategies | [Paid](/strategy/paid) |

## Used by
- **Workflows:** [Ad test setup](/capabilities/run-ads/ad-test-setup)
- **Agent tasks:** [Ad kit](/capabilities/make-content/ad-kit) · [Message test](/capabilities/run-ads/message-test)

## Reference

| Call | Path | Price |
| --- | --- | --- |
| [`Audience.create`](/code/apps/ads/create-audience) | `POST /audiences/create` | Free |
| [`Campaign.create`](/code/apps/ads/create-campaign) | `POST /campaigns/create` | Free |
| [`Ad.create`](/code/apps/ads/create-ad) | `POST /ads/create` | Free |
| [`Campaign.update`](/code/apps/ads/update-campaign) | `POST /campaigns/update` | Free |
| [`Metric.query`](/code/apps/analytics-and-experiments/query) | `POST /metrics/query` | Free |
| [`Lead.qualify`](/code/apps/crm/qualify) | `POST /leads/qualify` | Free |