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

# Campaign Targeting

> Attach structured targeting intent to a campaign through the V3 MCP surface

## Overview

You can attach structured **targeting intent** to a campaign when creating or updating it through the V3 MCP surface (`save_campaign` on `/mcp/v3`). Targeting records what the campaign *intends* to reach — geography, language, device type, daypart, and demographic dimensions — and persists that intent with the campaign object.

> [!NOTE]
>
> Campaign targeting is stored and returned on the campaign workspace. When you
> create a media buy with `save_media_buy`, its targeting can narrow the V3
> campaign overlay but can never widen it.

## Passing targeting to save\_campaign

Campaign targeting is the `targetingOverlay` parameter, the AdCP targeting
overlay with canonical codes. It is optional. Omit it to leave targeting
unchanged; send it to set the campaign's targeting:

```json theme={null}
{
  "targetingOverlay": {
    "geo_countries": ["US"],
    "geo_metros": [{ "system": "nielsen_dma", "values": ["501"] }],
    "device_type": ["ctv"],
    "language": ["en"]
  }
}
```

Each field is the AdCP field of the same name: countries and regions as ISO
3166 codes, DMAs as numeric Nielsen codes, postal areas, places, proximity,
language, device type and platform, browser, dayparts, demographics and age
restriction. A DMA name is refused with an error that names its code, or,
when the name matches more than one DMA (for example "Portland"), lists every
candidate so you can ask the buyer which one they mean.

The requirement-shaped `targeting` parameter (`requirementId`, `strength`) has
been removed. A call that sends it is refused with `VALIDATION_ERROR`.

## Reading targeting back

After save, `targetingOverlay` appears on the campaign workspace returned by
`save_campaign` and by `get(kind: "campaign")`. A campaign with no targeting set
omits the field.

## Updating targeting

Pass `targetingOverlay` in a `save_campaign` update to replace the campaign's
targeting. Each call is the full desired overlay, not a patch. Send `null` to
clear it.

## Targeting stated in the brief

The campaign brief reaches every seller in a proposal round. Sellers read it to
understand the campaign and to choose what to recommend, for example inventory
suited to an age group or an age signal they sell.

A hard requirement stated only in the brief, such as "please target women
18-55", is not applied the same way by every seller:

* [Storefront-built and Agent-supplied storefronts](https://docs.apostra.com/v2/guides/discovery.md#storefront-agents-in-discovery)
  read it from the brief and never refuse a request only because the brief
  states targeting. Whether it restricts delivery or is used only to recommend
  products depends on the AdCP version of the request; see
  [Exact Age Targeting](https://docs.apostra.com/v2/guides/age-targeting.md#overview).
* Built-in platform adapters read one closed age range from the brief.
* Other sellers interpret the brief with their own sales agent.

AdCP asks buyers to put every requirement that has a structured field into that
field, so the requirement does not depend on each seller reading prose. Set
hard targeting in the campaign's `targetingOverlay` and keep the brief as
written. The brief still goes to every seller as context.

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "expectedRevision": 3,
  "idempotencyKey": "acme-autumn-targeting-v1",
  "targetingOverlay": {
    "geo_countries": ["US"],
    "demographics": {
      "age": { "kind": "range", "min": 18, "max": 55, "include_unknown": false }
    }
  }
}
```

* `age.kind` says which bounds the age has: `range` (both `min` and `max`),
  `minimum` (`min` only) or `maximum` (`max` only). It is required.
* `targetingOverlay` replaces the campaign's stored overlay. Send the fields
  already set, such as the campaign's countries, together with the new one.
* Only move a requirement that has a field. A requirement with no structured
  field, such as gender, stays in the brief only.
* A preference ("ideally 18-55") or a description of the audience ("our
  customers are mostly in their 30s") stays in the brief. A structured field
  is applied as a restriction wherever the seller supports it.
* Keep the brief and the structured fields consistent. AdCP 3.2 requires a
  seller to refuse a request whose brief contradicts its structured targeting.

Structured targeting changes what you get back. Proposal rounds send the
campaign's age range to storefronts that read it, and those storefronts return
only products that can deliver that range. At launch, a campaign age range
reaches only the sellers that can apply it; see
[Campaign age range at launch](https://docs.apostra.com/v3/campaign-targeting.md#campaign-age-range-at-launch).

## Campaign targeting and media buys

`save_media_buy` applies the campaign targeting overlay as a guardrail to each
product's `targetingOverlay`. An explicit product `targetingOverlay` supplies
that buy's targeting, and campaign targeting still narrows it: overlapping
values are kept, and a product value outside the campaign returns `CONFLICT`.
A product can omit a campaign field to inherit it, or narrow it where the field
has defined containment rules. It cannot widen the campaign audience.

`signal_targeting` is excluded from the overlay because it is a priced,
product-scoped selection that will come through the signals path. Typed
`signal_targeting_groups` remains supported.

On a V3 media-buy product overlay, DMAs are numeric Nielsen codes. A DMA name
is refused with an error that names its code, and an unknown code returns
nearby code candidates.

For country, region, metro, postal, language, browser, device type, and device
platform includes, the buy uses the overlap. Excludes are combined. A buy's
demographic age range is intersected, unknown ages are included only when both
overlays allow them, and accepted age bases are intersected. The stricter age
restriction wins. Each buy daypart must fit wholly inside a campaign daypart
on the same clock; dayparts are never clipped or converted between timezones.

For proximity, named places, and other structured values without a containment
rule, a product must omit the field or repeat the campaign value exactly.

If the included values for the same field do not overlap, `save_media_buy`
returns a `CONFLICT` error. The error names the field and includes the campaign
and product values so you can correct one of them. It does not create a media
buy that targets everywhere. Omit a product geography field to inherit the
campaign's value for that field.

Language, device, daypart, and demographic campaign intent now participate in
this guardrail. A demographic overlay still cannot dispatch while seller
transport is on AdCP 3.1, so `save_media_buy` rejects it before seller contact.

### Postcodes on live media buys

Saving `targetingOverlay.geo_postal_areas` on a campaign also sends the
postcodes to the campaign's live media buys. Give every postal area its
`country` (or a country-specific system such as `au_postcode`): some sellers
look a postcode up by its bare code, which can exist in several countries.

Campaign postcodes replace a live buy's location targeting. A country, region,
or Nielsen DMA the same save does not set is removed from the live buy, so
changing a campaign from regions to postcodes leaves only the postcodes. Media
buys that have ended (completed, canceled, failed, or rejected) are left as
they are.
Removing postcodes from the campaign removes them from the live buy when the
buy still targets a country, region, or DMA afterwards, either one it already
had or one the same save sets. A save that would leave a live buy with no
location targeting is refused, because the buy would otherwise target every
location. Postcodes set through the V2 `constraints` field are stored on the
campaign but not sent to live buys.

Seller support varies. See each platform's page under
[Ad platform compatibility](https://docs.apostra.com/v2/guides/ad-platform-compatibility/index.md); for
example, [OpenAI Ads](https://docs.apostra.com/v2/guides/ad-platform-compatibility/openai-ads.md)
refuses a postcode together with the country or region that contains it, and
does not support postcode exclusion.

### Campaign age range at launch

A campaign's age range (`demographics.age`) is copied into every package when
the campaign launches. A media buy on a Meta or Spotify storefront that
approves media buys automatically is sent under AdCP 3.2, and the seller
applies the age range.

Every other seller cannot yet apply a campaign age range at launch. That
includes the other built-in platform adapters, a Meta or Spotify storefront
that reviews media buys manually, Storefront-built storefronts and external
AdCP sellers. Their packages are created without the age range, so the media
buy launches and runs without age targeting. The launch response says so with
a `demographics_not_applied` warning that names each affected product and
package `buyerRef`.

A product can carry its own fixed age audience. It counts only when the
product delivers that age to every impression, and when the age is an AdCP
included signal, only when the signal states how the ages are known in a way
the campaign accepts: verified or self-declared ages, plus modeled ages when
the campaign allows modeled age. A survey-based or modeled audience claim is
treated as no fixed audience (see
[Which products match a requested range](https://docs.apostra.com/v2/guides/age-targeting.md#which-products-match-a-requested-range)):

* **Fixed to exactly the campaign's range:** no warning; the product delivers
  it.
* **Fixed to a range inside the campaign's**, such as 18-34 for an 18-55
  campaign: the package is bought with the product's range, on every seller.
  The launch response confirms what was sent with a `demographics_narrowed`
  receipt naming the product and both ranges. That covers a product selected
  by id or a campaign range changed after you reviewed proposals.
* **Fixed to a range that reaches ages outside the campaign's**, such as 18-64
  for a 25-54 campaign: that media buy is refused unless its seller can apply
  the campaign's range, because it would deliver to people the campaign
  excludes.

## Campaign audiences and catalogs

`save_campaign` also accepts `audienceConfig` and `catalogId`. Use
`audienceConfig.targetAudienceIds` to include up to 100 audience IDs and
`audienceConfig.suppressAudienceIds` to exclude up to 100 audience IDs. On an
update, `deleteMissing: true` replaces both lists; otherwise, supplied IDs are
added to the existing lists. `catalogId` is the public catalog ID returned by
the catalog read surfaces; pass `null` on an update to remove it.

Both fields are stored and returned by `get(kind: "campaign")`.

**Audiences.** A campaign with audiences launches when every seller on it takes
mapped audiences. A seller takes them when its own audience support covers
everything a buy needs: finding the audience on the account, checking it is
ready, and targeting and excluding it. Meta, OpenAI Ads, Google and Reddit do
today. Map the audience to the seller
account first with `save_audience` and check it reads `synced`; see
[Map event sources and audiences](https://docs.apostra.com/v2/guides/map-event-sources-and-audiences.md).
A seller that does not take mapped audiences still blocks the launch, and the
campaign readiness response names that seller.

**Catalogs.** A campaign with a catalog launches when every live line sells
through a product that declares it takes that catalog's type. Otherwise the
readiness response returns `catalog_sync_unavailable`. Remove the catalog to
launch without it.

## Targeting each product offers

Each product says which targeting you may set on a package for it: its AdCP
`overlay_support`, such as countries, regions, Nielsen DMA metros or a
frequency cap, with any per-package limits the seller sets. You can book only
what a product offers.

Where you see it:

* **`get_products`** returns `overlay_support` on each product that declares
  targeting, in both wholesale and brief results. A product composed from
  inventory carries the declaration of the inventory product it is built from.
  See [The targeting a product lets you set](https://docs.apostra.com/v2/buyer/discovery/multi-storefront-get-products.md#the-targeting-a-product-lets-you-set).
* **V3 `search(kind: "wholesale_product")`** returns `overlaySupport` and
  `supportedTargeting` on each product that declares targeting.
  A declaration too large to return inline is left out of search, leaving
  `supportedTargeting`.
* **V3 `get(kind: "wholesale_product")`** returns one product, by the
  `productId` and `advertiserId` from your search, with its complete
  `overlaySupport`.
* **V3 `get(kind: "seller")`** returns the seller's own targeting (`targeting`
  and `supportedTargeting`). It applies to the seller's products that declare
  none of their own.

When you save a draft media buy, create one, or update its targeting, each
package's targeting is checked against its product's declaration. The
targeting checked is what the buy will send the seller, including campaign
targeting the package inherits. A package is refused when it targets a
dimension the product doesn't declare, a metro or postal system it doesn't
list, or more values than a per-package limit allows. Nothing is saved or sent
to the seller when a package is refused.

* **Through Apostra's buyer tools** (`save_media_buy`, `save_campaign`,
  `create_media_buys`) the refusal is a validation error on
  `targeting_overlay`, for example: "The selected products do not offer the
  requested targeting: package line-1 targets what product … does not offer:
  geo\_metros system uk\_itl1 is not offered by this product (offered:
  nielsen\_dma)."
* **Through a storefront's AdCP endpoint** the refusal is `UNSUPPORTED_FEATURE`
  with `details.reason` `undeclared_product_targeting` and the same message.

To fix it, remove the named targeting from the package, or from the campaign
when the package inherits it, or choose a product that offers it.

Gender targeting (`demographics.gender`, added in AdCP 3.3) is checked the same
way. A product offers it only by declaring `overlay_support.demographics.gender`;
`demographics: true` or an age-only declaration does not cover it. No product
declares gender yet, so a package that targets gender is refused, with
`demographics.gender is not offered by this product`, rather than sent to a
seller that would ignore it.

A product on a seller's managed sales agent that the seller hasn't narrowed
offers the targeting the seller's ad server can apply, and is checked against
that in the same way. A product on any other source that declares no targeting
of its own isn't checked this way. The seller's own targeting applies to it
instead, and a booking that asks for targeting the seller can't apply is
refused.

## Rollout and release check

This is available immediately on the public V3 `save_campaign` and
`get(kind: "campaign")` surfaces; it has no separate customer enrollment or
feature flag. The Buyer V3 API release owner verifies the release through the
automated `PR Acceptance Tests` catalog contract proof, then confirms in the
released environment that a campaign can save and read back an audience or
catalog and that a launch attempt returns the seller-sync blocker.

The release check passes only when all three behaviors agree: save persists the
requested IDs, `get` returns them, and launch stays refused while seller sync
is unavailable. Treat a missing readback value, a launch that succeeds, or a
blocker other than the seller-sync refusal as a mitigation trigger. Roll back
the release or remove the audience/catalog from the affected campaign; do not
override the blocker or treat the objects as delivered.

## Backward compatibility

V3 `save_campaign` no longer accepts the requirement-shaped `targeting`
parameter, and V3 campaign reads no longer return it; use `targetingOverlay`.
Existing campaigns keep their stored targeting, and their `targetingOverlay`
reflects it. The v2 campaign workspace response still returns `targeting` for a
campaign that stored it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.