URL: /code/apps/ads/create-audience · Id: `page:/code/apps/ads/create-audience`

# Audience.create

> Turn your waitlist or signups into an Audience on your ad account.

`POST https://api.sb/{your-domain}/audiences/create`

Turn your waitlist or signups into an [Audience](https://startupbuilder.dev/business/nouns/audience) on your ad account.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Audience.created`
- **Sites:** [7 sites](#on-each-venue)
- **Price:** Free

```text title="Run it · Ask"
Make a 1% lookalike of my waitlist on Facebook and Instagram, leaving out anyone suppressed.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Audience.create({ query: 'source = \'waitlist\' and suppressed = false', venue: 'facebook', lookalike: 1, name: 'Waitlist, 1% lookalike' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "audience": {
    "$type": "Audience",
    "name": "Waitlist, 1% lookalike",
    "query": "source = 'waitlist' and suppressed = false",
    "venue": "facebook",
    "lookalike": 1,
    "countries": [
      "US"
    ],
    "size": 632,
    "matched": null,
    "outside": null
  },
  "action": {
    "operation": "Audience.create",
    "mode": "try",
    "status": "done"
  }
}
```

Matches a list from your records, such as your waitlist, to an Audience on your ad account, every email hashed first. This Verb is a sketch on the hub’s [Audience](https://startupbuilder.dev/business/nouns/audience) Noun.</HubDefinition>

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `query` | `string` | Required | The saved query that picks the people, in the where grammar, such as `source = 'waitlist'`. It stays live, so the Audience follows your records. |
| `venue` | `string` | Required | Where to match it, by id, such as `facebook`, `linkedin` or `google-search`. |
| `lookalike` | `number \| null` | `null` | A lookalike of the matched list, from 1, the closest, to 10, as a percent of each country’s people. Unset: the list itself. |
| `countries` | `string[]` | `['US']` | The countries a lookalike is drawn from, as ISO 3166 codes. |
| `name` | `string` | `the query` | The name the ad account shows. |
| `refresh` | `'daily' \| 'never'` | `'daily'` | `daily` sends each day’s removals, then its additions, so someone who opts out leaves the Audience within a day. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `audience` | `Audience` | Never null. |
| `audience.size` | `integer`, in people | Never null: how many people the query picks today. |
| `audience.outside` | `json` | Null in try mode and until the ad account returns its own id for the Audience. |
| `audience.matched` | `integer \| null`, in people | Null until the ad account reports how many it matched, which can take hours. |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Counts who the query picks and checks each against Suppressions and opt-outs; nothing is uploaded.
- `send`: Returns a pending Action; `Audience.created` fires when the ad account reports how many it matched.
- `do`: Holds the call for the Mode’s hold time, then returns `202` with the pending Action if the match is still running.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. An Audience already on the account for the same query is updated, never uploaded twice: `outside` holds its id there.

**Saved or shared**

The Audience keeps its query. Addresses reach the ad account hashed, and only the matched count comes back. Nothing is shared with other startups.

**Emits**

`Audience.created` with `{ audience, venue, size, matched }`

**Performed by**

[Paid Media Manager](https://startupbuilder.dev/agents/marketing/paid-media-manager)

**Price**

Free on your own account: the platform charges nothing for the call.

## On each Venue
| Site | Fields | Formats |
| --- | --- | --- |
| [Google Search and Google Display and YouTube](#google-ads) | None. | 2 |
| [Google Analytics](#google-analytics) | `query` | None. |
| [LinkedIn](#linkedin-ads) | `query` | 3 |
| [Facebook and Instagram](#meta-ads) | `query`, `lookalike` | 5 |

### Google Search and Google Display and YouTube
Through [Google Ads](/code/integrations/google-ads).

Google Search: Responsive search ad YouTube: Video

### Google Analytics
Through [Google Analytics](/code/integrations/google-analytics).

| Our field | Google Analytics’s field |
| --- | --- |
| `Audience.query` | `audience filter clauses` |

- **A retargeting audience.** Fills only from visits after it is built, so build it before the traffic you want to reach.

### LinkedIn
Through [LinkedIn Ads](/code/integrations/linkedin-ads).

| Our field | LinkedIn Ads’s field |
| --- | --- |
| `Audience.query` | `dmpSegment list upload` (A company list by name and domain, or contacts with each email hashed before it leaves your records) |

- **Matched audiences.** Run from 300 matched members; under that the campaign refuses with `venue-rejected`.

Feed post Single image ad, 1200 × 628, 1200 × 1200 or 720 × 900 Document ad

### Facebook and Instagram
Through [Meta Ads](/code/integrations/meta-ads).

| Our field | Meta Ads’s field |
| --- | --- |
| `Audience.query` | `customaudience (subtype CUSTOM)` (Emails are hashed before they leave your records) |
| `Audience.lookalike` | `lookalike_spec.ratio` (1 is 0.01) |

Facebook: Feed image ad, 1440 × 1800, 4:5 Instagram: Feed image, 320 to 1,440 pixels wide, 4:5 to 1.91:1 Instagram: Carousel Instagram: Reel, 9:16 Instagram: Story, 9:16

## What try does
| Account | What try does |
| --- | --- |
| [Google Ads](/code/integrations/google-ads#try) | Sends every change with `validate_only` set: Google Ads checks the whole build and returns any error, and creates nothing. |
| [Google Analytics](/code/integrations/google-analytics#try) | Reads run for real, because a report changes nothing; `try` stores no rows, builds no audience and marks no event. |
| [LinkedIn Ads](/code/integrations/linkedin-ads#try) | Reads your ad account, the matched count of each audience and the targeting facets, and checks the campaign against LinkedIn’s budget minimums; LinkedIn has no validate-only call, so nothing is sent to it and nothing is created. |
| [Meta Ads](/code/integrations/meta-ads#try) | 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. |

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| An Audience upload with one suppressed or opted-out member, or any check that errors | [`audience-guard`](/code/refusals#check-audience-guard) | Remove the rows the Guard lists, then upload again; nothing serves until then. | [`Audience.update`](https://startupbuilder.dev/business/nouns/audience/update) |
| A member from a matched, licensed or bought source | [`first-party`](/code/refusals#check-first-party) | Use only your own Visitors and Contacts. | [`Audience.update`](https://startupbuilder.dev/business/nouns/audience/update) |
| A tag or a list upload for a browser with an opt-out signal or no Consent | [`opt-out`](/code/refusals#check-opt-out) | Nothing to do: tags stop and the member leaves every list. | [`Audience.update`](https://startupbuilder.dev/business/nouns/audience/update) |

Every call through an account can also meet [the shared account refusals](/code/refusals#accounts): [`grant-missing`](/code/refusals#grant-missing), [`needs-reauth`](/code/refusals#needs-reauth), [`account-not-connected`](/code/refusals#account-not-connected), [`over-mandate`](/code/refusals#over-mandate), [`quota-reached`](/code/refusals#quota-reached), [`venue-rejected`](/code/refusals#venue-rejected).

```json
{
  "type": "https://startupbuilder.dev/errors/audience-guard",
  "title": "Upload has a suppressed member",
  "status": 422,
  "detail": "An Audience upload with one suppressed or opted-out member, or any check that errors.",
  "fix": "Remove the rows the Guard lists, then upload again; nothing serves until then.",
  "next": {
    "verb": "Audience.update",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
| Fact | Value |
| --- | --- |
| `send` returns | A pending Action, `reason: 'awaitingSource'`, until the ad account reports its match count. |
| Delivered by | `Audience.created` |

## Example

**Ask**

```text
Make a 1% lookalike of my waitlist on Facebook and Instagram, leaving
out anyone suppressed.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/audiences/create?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"source = '\''waitlist'\'' and suppressed = false","venue":"facebook","lookalike":1,"name":"Waitlist, 1% lookalike"}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Audience.create --try --data '{"query":"source = '\''waitlist'\'' and suppressed = false","venue":"facebook","lookalike":1,"name":"Waitlist, 1% lookalike"}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Audience.create({ query: 'source = \\'waitlist\\' and suppressed = false', venue: 'facebook', lookalike: 1, name: 'Waitlist, 1% lookalike' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Audience.create({
  query: 'source = \'waitlist\' and suppressed = false',
  venue: 'facebook',
  lookalike: 1,
  name: 'Waitlist, 1% lookalike',
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `query: 'test = suppressed-member'` | Refuses with [`audience-guard`](/code/refusals#check-audience-guard) | One of its three test members sits on a Suppression, so nothing uploads until that member leaves the query. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

## Used by
- **Workflows:** [Build a verified list](/capabilities/find-customers/build-a-verified-list) · [Buying-trigger feed](/capabilities/find-customers/buying-trigger-feed) · [Lookalikes](/capabilities/find-customers/lookalikes) · [Post-engager harvest](/capabilities/find-customers/post-engagers)
- **Jobs:** [Find your first customers](/capabilities/find-customers) · [Run ads](/capabilities/run-ads)
- **Integrations:** [Google Ads](/code/integrations/google-ads) · [Google Analytics](/code/integrations/google-analytics) · [LinkedIn Ads](/code/integrations/linkedin-ads) · [Meta Ads](/code/integrations/meta-ads)

## Related
[`Campaign.create`](/code/apps/ads/create-campaign) · [`Campaign.update`](/code/apps/ads/update-campaign) · [`Ad.create`](/code/apps/ads/create-ad)