# Targeting

Target ads by location, age, interests, keywords, job titles or subreddits, look up each network's ids with one search endpoint and estimate reach first.

Targeting tells a network who should see an ad. Pozy uses one set of field names across networks, then translates them into each network's own targeting spec. Portable fields such as `countries`, `ageMin`, `ageMax`, `gender` and `languages` work everywhere. The rest only mean something on the network they were looked up for, and a field a network cannot honor is rejected with a `400` that names it.

## Where targeting goes

| Call | Shape |
| --- | --- |
| `POST /v1/ads/create` | Flat fields at the top level (Meta, TikTok, Pinterest, X, Reddit), or a nested `targeting` object (LinkedIn, Google accepts both) |
| `POST /v1/ads/boost` | Inside `targeting` |
| `POST /v1/ads/messaging`, `POST /v1/ads/call` | Flat fields, like create |
| `PUT /v1/ads/{adId}` | `targeting` updates the ad's ad set or campaign where the network allows it |
| `PUT /v1/ads/campaigns/{campaignId}/targeting` | Google campaign-level devices, locations and languages |
| `POST /v1/ads/targeting/reach-estimate` | Inside `spec` |

## Look up ids

Cities, regions, interests, job titles and subreddits are ids that belong to each network. Find them with `GET /v1/ads/targeting/search`, using the `accountId` of the ad connection you will create the ad on.

```bash cURL
curl -G https://api.pozy.ai/v1/ads/targeting/search \
  -H "Authorization: Bearer $POZY_API_KEY" \
  --data-urlencode "accountId=66b2e19d8c3f5a7e9d0b1c2d" \
  --data-urlencode "q=Amsterdam" \
  --data-urlencode "dimension=geo" \
  --data-urlencode "geoType=city" \
  --data-urlencode "countryCode=NL"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/ads/targeting/search?accountId=66b2e19d8c3f5a7e9d0b1c2d&q=Amsterdam&dimension=geo&geoType=city&countryCode=NL", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
const cityKey = data.results[0].id; // send as { key: cityKey }
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/ads/targeting/search",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"accountId": "66b2e19d8c3f5a7e9d0b1c2d", "q": "Amsterdam", "dimension": "geo", "geoType": "city", "countryCode": "NL"},
)
print(resp.json()["results"][0])
```

```json
{
  "results": [
    { "id": "2759794", "name": "Amsterdam", "type": "city", "path": ["Netherlands", "North Holland", "Amsterdam"], "audienceSize": null }
  ]
}
```

Send a geo result's `id` as `key` in `cities`, `regions`, `zips` or `metros`. A `country` result's `id` is the ISO code for `countries`. Interest and behavior results go in `interests` and `behaviors` as `{ id, name }`.

| `dimension` | Meta | Google | TikTok | LinkedIn | X | Reddit |
| --- | --- | --- | --- | --- | --- | --- |
| `geo` | Yes | Yes | Yes | Yes | Yes | Yes |
| `interest` | Yes | Yes (affinity and in-market) | Yes | Yes | Yes | Yes |
| `behavior` | Yes | – | Yes | Yes | – | – |
| `language` | – | Yes | – | – | – | – |
| `income` | Yes | Yes (`top_10` only) | – | – | – | – |
| `workPosition`, `workEmployer`, `workIndustry` | Yes | – | – | – | – | – |
| `industry`, `jobFunction`, `seniority`, `companySize`, `skill` | – | – | – | Yes | – | – |
| `community`, `conversationKeyword` | – | – | – | – | – | Yes |

`geoType` narrows geo searches to `country`, `region`, `city` (the default), `subcity`, `neighborhood`, `place`, `zip`, `metro_area` or `geo_market`. Google and LinkedIn ignore it and return every level, so filter on each result's `type`. `GET /v1/ads/targeting/browse` walks Meta's targeting category tree (by `type` and `parentNodeId`) instead of searching it. `dimension` defaults to `interest`. A dimension the network does not support returns `400` or `404`.

## Estimate reach first

`POST /v1/ads/targeting/reach-estimate` returns an audience size range before you create anything:

```json
{
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "adAccountId": "act_1234567890",
  "spec": { "countries": ["NL"], "ageMin": 25, "ageMax": 54, "interests": [{ "id": "6003107902433", "name": "Interior design" }] }
}
```

```json
{ "available": true, "lower": 412000, "upper": 585000, "estimateReady": true }
```

`estimateReady: false` means the network is still computing, which happens with brand-new audiences: retry shortly. Networks without an estimate return `available: false`. On LinkedIn, [bid pricing and supply forecasts](https://pozy.ai/docs/ads/linkedin-ads#forecast-before-you-spend) add bid ranges and projected delivery.

## Meta

| Field | Values |
| --- | --- |
| `countries`, `countryGroups` | ISO codes, or groups such as `eea` or `north_america`. Defaults to `["US"]` only when no geo field is sent. |
| `cities` | `{ key, radius, distance_unit }`. Meta's minimum radius is about 17 km (10 miles); smaller resolves to an empty audience. |
| `regions`, `zips`, `metros` | `{ key }` from the search, for example `US:94304` or `DMA:807` |
| `customLocations` | `{ latitude, longitude, radius, distanceUnit }` |
| `excludedLocations` | Countries, regions, cities, ZIPs, places, neighborhoods, custom locations |
| `ageMin`, `ageMax` | 13 to 65 |
| `gender` | `all`, `male`, `female` |
| `interests`, `behaviors` | `{ id, name }` |
| `workPositions`, `workEmployers`, `workIndustries` | `{ id, name }` |
| `incomeTier` | `top_5`, `top_10`, `top_10_25`, `top_25_50` (household income by ZIP) |
| `languages` | `en` targets every variant, `en_GB` one |
| `userOs`, `userDevice` | For example `iOS_ver_14.0_and_above`, `iPhone` |
| `audienceInclude`, `audienceExclude` | Audience ids ([Audiences](https://pozy.ai/docs/ads/audiences)) |
| `placements` | Manual placements. Omit for automatic placements. |
| `savedTargetingId` | A saved targeting audience as the base, with inline fields on top |
| `rawTargeting` | A Meta-native spec for anything the flat fields do not cover, such as layered `flexible_spec` |
| `advantageAudience` | `0` keeps targeting strict, `1` lets Meta expand |

> **Locations overlap:**
> 
> Do not send a city together with the country that contains it. Meta rejects the ad. Drop the country, or use a different one.

Housing, employment and credit ads (`specialAdCategories`) cannot use income or ZIP targeting. EU delivery needs `dsaBeneficiary` and `dsaPayor`.

## Google

| Field | Values |
| --- | --- |
| `countries` | ISO codes. Search and Display default to `US` with no geo; Performance Max and Demand Gen run worldwide. |
| `regions`, `cities`, `zips`, `metros` | `{ key }`, Google's geo target ids |
| `customLocations` | A pin with radius (Google has no city radius) |
| `excludedLocations` | Countries, regions, cities, ZIPs |
| `languages` | ISO 639-1 codes plus variants such as `zh_CN` |
| `ageMin`, `ageMax`, `gender` | Search and Display only, 18 and over |
| `incomeTier` | `top_10` only |
| `interests` | Affinity and in-market segments, Search and Display |
| `audienceInclude`, `audienceExclude` | User list ids, such as Customer Match lists |
| `keywords`, `negativeKeywords` | Search only, see [Google Ads](https://pozy.ai/docs/ads/google-ads#keywords) |

`locationTargetingType` sets whether locations reach people physically there (`presence`) or also people interested in them (`presence_or_interest`, Google's default). Performance Max and Demand Gen take geo and language only. On a running campaign, `PUT /v1/ads/campaigns/{campaignId}/targeting` replaces `devices` (`MOBILE`, `DESKTOP`, `TABLET`, `CONNECTED_TV`, each with an optional bid modifier), `locations`, `excludedLocations` and `languages`.

## TikTok

| Field | Values |
| --- | --- |
| `countries` | ISO codes. Required on boosts. |
| `regions`, `cities`, `zips`, `metros` | Create only |
| `ageMin`, `ageMax`, `gender`, `languages` | `zh_TW` targets traditional Chinese |
| `interests` | From the interest search |
| `behaviors` | `video:<id>` or `creator:<id>`. Restricts delivery to the TikTok placement. |
| `incomeTier` | As on Meta |
| `audienceInclude`, `audienceExclude` | Processed custom audiences |

`PUT /v1/ads/{adId}` with `targeting` is a partial update; fields you leave out keep their value, except that a new location replaces both location and ZIP targeting.

## LinkedIn

| Field | Values |
| --- | --- |
| `countries`, `regions` | ISO codes or `urn:li:geo:*`. At least one location; none means the United States. |
| `industries`, `seniorities`, `jobFunctions`, `jobTitles`, `skills` | Id fragments or full URNs |
| `companySizes` | `A` to `I` staff-count ranges |
| `interests`, `behaviors` | URNs from the search |
| `audienceInclude`, `audienceExclude` | Matched audience segment ids |
| `ageMin`, `ageMax`, `gender`, `languages` | Rounded to LinkedIn's buckets |
| `excludedLocations.countries` | ISO codes |

`jobTitles` cannot be combined with `seniorities` or `jobFunctions`. Cities, ZIP codes, radius and income return `400`. Updates through `PUT /v1/ads/{adId}` merge facet by facet; send a facet empty to remove it.

## Pinterest

`countries`, `regions`, `metros`, `zips`, `ageMin`, `ageMax`, `gender`, `languages`, `interests` (from `GET /v1/ads/interests`) and customer list audiences. `regions` and `zips` cannot be combined. Targeting cannot change after create: recreate the ad.

## X

`countries`, `regions`, `metros`, `cities`, `zips`, `interests`, Tailored Audiences, `gender` and `languages`. Age rounds out to X's fixed ranges (starting at 13, 18, 21, 25, 35 or 50 and ending at 24, 34, 49 or 54, or open-ended), so `ageMin: 20, ageMax: 40` targets 18 to 49. Income, custom locations and behaviors are rejected. Targeting cannot change after create.

## Reddit

| Field | Values |
| --- | --- |
| `countries`, `regions`, `metros`, `cities`, `zips` | No location means US |
| `excludedLocations` | Countries, regions, cities and ZIPs |
| `gender`, `languages` | |
| `targeting.interests` | From the interest search |
| `targeting.communities`, `targeting.excludedCommunities` | Subreddit names, with or without `r/` |
| `targeting.conversationKeywords`, `targeting.excludedConversationKeywords` | Free text |
| `audienceInclude`, `audienceExclude` | Custom audiences built in Reddit Ads Manager |

Age, income and radius are not available. Targeting can be changed after launch with `PUT /v1/ads/{adId}`.

## ChatGPT

ChatGPT ads target by country only.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/ads/interests`](https://pozy.ai/docs/api/searchAdInterests) | Search targeting interests |
| [`GET /v1/ads/targeting/search`](https://pozy.ai/docs/api/searchAdTargeting) | Search targeting options |
| [`GET /v1/ads/targeting/browse`](https://pozy.ai/docs/api/browseAdTargeting) | Browse targeting categories |
| [`POST /v1/ads/targeting/reach-estimate`](https://pozy.ai/docs/api/estimateAdReach) | Estimate audience reach |
| [`POST /v1/ads/targeting/bid-pricing`](https://pozy.ai/docs/api/getLinkedInBidPricing) | Suggested bid and budget bounds |
| [`POST /v1/ads/targeting/supply-forecast`](https://pozy.ai/docs/api/getLinkedInSupplyForecast) | Forecast ad delivery |
