# Audiences

Build customer lists, lookalikes, website, engagement and company audiences on each ad network, upload hashed members and target them.

An audience is a set of people you target or exclude. Some you fill yourself by uploading emails (customer lists). Others the network fills from a pixel, a seed list or engagement with your Page or videos. Create every type with `POST /v1/ads/audiences`, then put its `platformAudienceId` in `audienceInclude` or `audienceExclude` on a create.

## Upload a customer list

### Create the audience

```bash cURL
curl -X POST https://api.pozy.ai/v1/ads/audiences \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "act_1234567890",
    "name": "Customers - last 12 months",
    "type": "customer_list",
    "description": "Paid at least once since November 2025"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/ads/audiences", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "66b2e19d8c3f5a7e9d0b1c2d",
    adAccountId: "act_1234567890",
    name: "Customers - last 12 months",
    type: "customer_list",
    description: "Paid at least once since November 2025",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
const audienceId = data.audience.id;
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/ads/audiences",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
        "adAccountId": "act_1234567890",
        "name": "Customers - last 12 months",
        "type": "customer_list",
        "description": "Paid at least once since November 2025",
    },
)
audience_id = resp.json()["audience"]["id"]
```

Creates are not idempotent. Do not retry one automatically: list the audiences first to see whether it exists.

### Upload members

Send plaintext `email` (and on Meta, `phone` in E.164) per row, up to 10,000 rows per request.

```bash
curl -X POST https://api.pozy.ai/v1/ads/audiences/66e5b2c3d4f5a6b7c8d9e0f1/users \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "users": [ { "email": "maria@example.com" }, { "email": "sam@example.org", "phone": "+14155551234" } ] }'
```

```json
{ "message": "Users added", "numReceived": 2, "numInvalid": 0 }
```

### Target it

Read `platformAudienceId` from `GET /v1/ads/audiences?accountId=...&adAccountId=...` and send it in `audienceInclude` or `audienceExclude` on `POST /v1/ads/create`.

### Hashing and matching

You send plain values. Pozy normalizes them (trims, lowercases emails, formats phones) and hashes them with SHA-256 before they leave Pozy, so the networks never receive raw contact details. You do not need to hash anything yourself.

| Network | Matches on | Per request | Upload behavior | Notes |
| --- | --- | --- | --- | --- |
| Meta | Email and phone | 10,000 | Adds | Remove people with `DELETE /v1/ads/audiences/{audienceId}/users` (Meta only). |
| Google (Customer Match) | Email | 10,000 | Adds | Matching takes 10 minutes to 24 hours. Google only serves a list once enough members match; upload at least 5,000. |
| TikTok | Email | 10,000 | Adds | Created on TikTok with the first upload. Takes up to 48 hours before it can be targeted. |
| LinkedIn | Email | 10,000 | Replaces the whole list | Every upload is the full list, so a contact list tops out at 10,000 members. |
| Pinterest | Email | 10,000 | Adds | Created on Pinterest with the first upload. |
| X (Tailored Audiences) | Email | 10,000 | Adds | Needs at least 100 matched active users before it can be targeted; size reads 0 until then. |
| Reddit | – | – | – | Build custom audiences in Reddit Ads Manager and target them by id. Creates return `422`. |
| ChatGPT | – | – | – | No custom audiences. |

A `phone` on a network that matches on email only is ignored. A row that cannot be read counts in `numInvalid`.

> **Consent:**
> 
> Only upload people who agreed to be contacted this way. Each network's terms for customer lists apply to you, and the hashed upload does not change that.

## Audience types

`type` on `POST /v1/ads/audiences` picks what the audience is built from.

| `type` | Network | Built from | Key fields |
| --- | --- | --- | --- |
| `customer_list` | Meta, Google, TikTok, LinkedIn, Pinterest, X | Emails (and phones on Meta) you upload | `customerFileSource` on Meta |
| `lookalike` | Meta | People similar to a seed audience | `sourceAudienceId` (the seed's `platformAudienceId`), `country`, `ratio` 0.01 to 0.2 |
| `website` | Meta | Pixel visitors | `pixelId`, `retentionDays`, optional `urlContains` or a raw Meta `rule` |
| `meta_engagement` | Meta | People who engaged with a Page, an Instagram account or a video | `engagementSource` (`page`, `instagram`, `video`), `sourceId`, `retentionDays` 1 to 365, optional `event` |
| `saved_targeting` | Meta | A stored targeting spec, no members | `spec`. Use it later as `savedTargetingId`. |
| `company_list` | LinkedIn | Companies by name, domain, website or Page URL | `companies` (up to 300,000 rows, at least 1,000 recommended) |
| `engagement` | LinkedIn | People who engaged with ads, Pages or events | `sourceType`, `trigger`, `lookbackDays` (30 to 365), `engagementSources` (up to 50) |
| `website_retargeting` | LinkedIn | Insight Tag visitors matching URL rules | `matchRules` with `matchType` (`EXACT`, `STARTS_WITH`, `CONTAINS`, `ENDS_WITH`), up to 50 rules |
| `tiktok_engagement` | TikTok | People who engaged with your ads or videos | `source`, `sourceIds`, `identityId` |
| `pinterest_engagement` | Pinterest | People who engaged with your Pins | `engagementType`, `pinIds` |

Using a type on the wrong network's account returns `422`. Meta's `meta_engagement` and LinkedIn's `engagement` are different audiences with different fields.

### Lookalikes on Meta

Upload members to the seed list first, then create the lookalike from it. Meta builds it from the seed's matched members, so check the seed's `size` with `GET /v1/ads/audiences/{audienceId}` before you create. `ratio: 0.01` is the closest 1% of the country.

```json
{
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "adAccountId": "act_1234567890",
  "name": "Lookalike 1% US - customers",
  "type": "lookalike",
  "sourceAudienceId": "23851234567890123",
  "country": "US",
  "ratio": 0.01
}
```

### LinkedIn company lists

Company lists target or exclude whole companies. Each row needs at least one of `name`, `domain`, `website` or `linkedinPageUrl`. LinkedIn matches rows against its company graph within 48 hours. To change the list, send the complete new list to `POST /v1/ads/audiences/{audienceId}/companies`: it replaces the old one.

### LinkedIn website retargeting

The audience fills only once the site has the LinkedIn Insight Tag installed and reporting visits ([Pixels and tags](https://pozy.ai/docs/ads/pixels)). You can create it before the tag is live.

## Manage audiences

| Job | Call |
| --- | --- |
| List with size and status | `GET /v1/ads/audiences?accountId=...&adAccountId=...` |
| Read one with fresh numbers | `GET /v1/ads/audiences/{audienceId}` |
| Rename or edit | `PUT /v1/ads/audiences/{audienceId}` (name and description; `spec` only on saved targeting) |
| Delete | `DELETE /v1/ads/audiences/{audienceId}` |

`audience.id` is Pozy's id, used in the paths above. `platformAudienceId` is the network's id, used in targeting and as a lookalike seed. Audiences build asynchronously on every network: a size or member read right after an upload can be empty. Read again after a minute instead of uploading again.

## Errors

| Error | Cause | Fix |
| --- | --- | --- |
| `422` on `/users` | The audience is not a `customer_list` (or is a LinkedIn company list) | Upload rows only to customer lists. Company lists use `/companies`. |
| `422` "Audience has no platform ID" | The network has not accepted the audience yet | Read it again and retry once `platformAudienceId` is set. |
| `422` on create | The type does not exist on that network | Use a type from the table above. |
| `400` with `type: "platform_error"` | The network rejected a rule or source (for example an ineligible engagement source on Meta) | Read the network's message in `platformError`. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/ads/audiences`](https://pozy.ai/docs/api/listAdAudiences) | List custom audiences |
| [`POST /v1/ads/audiences`](https://pozy.ai/docs/api/createAdAudience) | Create custom audience |
| [`GET /v1/ads/audiences/{audienceId}`](https://pozy.ai/docs/api/getAdAudience) | Get audience details |
| [`PUT /v1/ads/audiences/{audienceId}`](https://pozy.ai/docs/api/updateAdAudience) | Update an audience |
| [`DELETE /v1/ads/audiences/{audienceId}`](https://pozy.ai/docs/api/deleteAdAudience) | Delete custom audience |
| [`POST /v1/ads/audiences/{audienceId}/users`](https://pozy.ai/docs/api/addUsersToAdAudience) | Add users to audience |
| [`DELETE /v1/ads/audiences/{audienceId}/users`](https://pozy.ai/docs/api/removeUsersFromAdAudience) | Remove users from audience |
| [`POST /v1/ads/audiences/{audienceId}/companies`](https://pozy.ai/docs/api/replaceAdAudienceCompanies) | Replace audience companies |
