> ## Documentation Index
> Fetch the complete documentation index at: https://docs.semicola.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Targeting

> Record campaign targeting intent, set campaign constraints, and change a package targeting overlay on a live buy.

Targeting in Semicola lives in three places:

| Where                               | What it does                                                                                               |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Targeting intent** (campaign)     | Records what the campaign intends to reach. Stored and returned on the campaign; not enforced at delivery. |
| **Campaign constraints** (campaign) | Countries and DMAs that flow into every package's targeting overlay sent to sellers.                       |
| **Package targeting overlay**       | The AdCP targeting a seller receives for one package. Change it on a live buy with `update_media_buy`.     |

## Targeting intent on save\_campaign

Pass `targeting` to `save_campaign` with one or more of `geo`, `language`, `device`, `dayparts` and
`demographics`. Each dimension is a list of **requirements**:

```json theme={null}
{
  "targeting": {
    "geo": [{ "requirementId": "geo-us-only", "strength": "required", "include": ["US"] }],
    "device": [{ "requirementId": "ctv-preferred", "strength": "preferred", "include": ["ctv"] }],
    "language": [{ "requirementId": "en-only", "strength": "required", "include": ["en"] }]
  }
}
```

| Field           | Notes                                                                        |
| --------------- | ---------------------------------------------------------------------------- |
| `requirementId` | Your stable id for the requirement. It must be unique across all dimensions. |
| `strength`      | `required` (must satisfy) or `preferred` (optimize toward).                  |
| `include`       | Values to target (at least one when present).                                |
| `exclude`       | Values to exclude (optional).                                                |

| Dimension      | Values                                                                                                                              |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `geo`          | ISO 3166 country codes (`US`) or region codes (`US-CA`).                                                                            |
| `language`     | BCP 47 tags (`en`, `fr-CA`).                                                                                                        |
| `device`       | `desktop`, `mobile`, `tablet`, `ctv`, `dooh`, `unknown`.                                                                            |
| `dayparts`     | A `timezone` (IANA, for example `America/New_York`) and `windows` of `days` (`mon`…`sun`), `startHour` (0–23) and `endHour` (1–24). |
| `demographics` | `age`: requirements with `minAge`, `maxAge`, `includeUnknownAge` and `allowModeledAge` (both default `false`).                      |

The intent comes back as `targeting` on the campaign workspace. A `save_campaign` update that passes
`targeting` replaces the whole stored intent: each call is the full desired state, not a patch.

<Note>
  Targeting intent is stored and returned but **isn't enforced at delivery**. What sellers receive is
  the package targeting overlay below.
</Note>

## Campaign constraints

`save_campaign` also accepts the simpler constraint form:

```json theme={null}
{ "targeting": { "countries": ["US"], "geoMetros": ["803"], "ageRange": { "min": 25, "max": 54 } } }
```

`countries` and `geoMetros` flow into every package's overlay
when a media buy is sent: include lists intersect with the package's own, exclude lists add together.
If the two include lists contradict each other, the campaign's list wins rather than widening to
"anywhere". `ageRange` and `audience` go into the brief text sellers read during discovery; `channels`
becomes a channel filter on product discovery.

## Package targeting overlays

Change a package's targeting on a draft or live buy with `update_media_buy`
(`PATCH /api/v2/buyer/media-buys/{id}`):

```json theme={null}
{
  "mediaBuyId": "…",
  "packages": [
    {
      "packageId": "…",
      "targetingOverlay": {
        "geo_countries": ["US"],
        "geo_metros": [{ "system": "nielsen_dma", "values": ["803", "501"] }],
        "device_type": ["ctv"]
      }
    }
  ],
  "reason": "Focus the flight on two markets"
}
```

The typed overlay fields are `geo_countries`, `geo_regions`, `geo_metros`, `audience_include`,
`device_type`, `language` and the `_exclude` variants. Every other AdCP overlay field passes through
and is validated against the AdCP schema on save.

* **Per-key merge.** Keys you name replace the package's current values; an empty list clears that key.
  Keys you omit stay as they are. Omitting `targetingOverlay` leaves targeting alone.
* **Seller support is checked first.** If the seller declared which targeting it supports and your
  overlay uses a dimension outside that list, the update is refused with `CAPABILITY_NOT_SUPPORTED`
  before anything is sent: "Sales agent does not support the requested targeting: … Remove these
  targeting fields or use a sales agent that declares support." Product discovery returns each
  product's declared list as `supported_targeting`.
* **Some sellers must approve.** The result may carry an [update proposal](/buy/update-proposals).

## Nielsen DMAs

DMA targeting uses `geo_metros` with `system: "nielsen_dma"` and three-digit codes such as `"803"`.

<Warning>
  Nielsen DMA market names aren't available yet (they're licensed data). Codes are accepted and saved
  as **unverified**, and the result's `warnings` say so. Names can't be matched until the dictionary
  lands, so send codes.
</Warning>

The dictionary tools are there for when names arrive; today they return an empty list with a `note`:

| MCP tool                          | REST (under `/api/v2/buyer`)                     |
| --------------------------------- | ------------------------------------------------ |
| `list_targeting_dimensions`       | `GET /targeting/dimensions`                      |
| `list_targeting_dimension_values` | `GET /targeting/dimensions/{system}`             |
| `resolve_targeting_dimension`     | `GET /targeting/dimensions/{system}/resolve?q=…` |
| `list_geo_metros`                 | `GET /targeting/geo-metros`                      |

`nielsen_dma` is the only `system`. When names are available, a name that could mean more than one DMA
is refused with the matching codes to choose from.

## Age ranges in briefs

Write one exact, closed age range in the brief ("adults ages 25–54"). Semicola's hosted storefronts
copy the numbers from the brief. If none of the proposed products can deliver that exact range, the
proposal carries a `limitations` entry for `targeting.age` with the closest supported range, and says
the age targeting was not applied. It never widens or approximates the range silently.

Per-platform exact-age rules for the ad platforms (Meta, Google Ads, TikTok and others) aren't
available: those sellers are sandbox storefronts with no live connection yet.

## Related

* [Campaigns](/buy/campaign)
* [Update proposals](/buy/update-proposals)
* [Product discovery](/buy/product-discovery)
