> ## 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.

# Exact Age Targeting

> Request one exact age range in product discovery and preserve it through media-buy execution and readback

## Overview

Exact age targeting reaches a storefront in one of two ways, depending on the
kind of storefront.

**Storefronts on the Storefront-built or Agent-supplied path** (see
[Storefront agents in discovery](/v2/guides/discovery#storefront-agents-in-discovery)) take the exact range from the AdCP 3.2
`targeting_overlay.demographics.age` field on a `get_products` request:

```json theme={null}
{
  "buying_mode": "brief",
  "brief": "Build an awareness campaign in the United States.",
  "targeting_overlay": {
    "demographics": {
      "age": { "min": 21, "max": 35, "include_unknown": false }
    }
  }
}
```

A storefront reads the structured field only from a request served at AdCP
3.2. When you set demographic targeting on a campaign, Apostra's proposal
requests send it to these storefronts as this field. Built-in platform adapters
and external sellers receive the brief only.

These storefronts take hard age targeting **only** from the structured field.
A decision model reads every brief and decides whether it *requires or
excludes* an age range, says whether to reach people whose age is unknown, or
only *describes* the audience. To make that decision, the storefront sends the
brief text to Cloudflare Workers AI, which runs the Clef decision model; no
other request data is sent. Ages count however
they are written, in digits, in words, or in another language. A brief that
states a hard age requirement is refused rather than guessed at, so move the
range into the structured field, or say in the brief that it is a preference:

| Brief wording | Result |
| - | - |
| `Target ages 25-54.`, `Target ages twenty-five to fifty-four.` or `Exclude anyone under 25.` | `INVALID_REQUEST` on `brief`: send the range in `targeting_overlay.demographics.age` (or a legal minimum age in `targeting_overlay.age_restriction`) and remove it from the brief. |
| `Include people whose age is unknown.` | `INVALID_REQUEST` on `brief`: set `include_unknown` in `targeting_overlay.demographics.age` instead. |
| `Ideally women 18-55, but this is not a hard requirement.` | No age restriction. The brief goes to the storefront as written. |
| `Our customer is generally in her 30s.` | No age restriction; a description of the audience. |
| `Budget 2500 EUR over 20 dagen.` | No age restriction; the number is a duration. |

A brief that restates the structured range as a requirement is refused in the
same way: keep hard targeting in the structured field only. A request served
at AdCP 3.1 has no structured age field, so until these storefronts negotiate
AdCP 3.2 publicly, a 3.1 caller can express age only as a preference in the
brief. If the brief
cannot be read at that moment, the storefront returns a retryable
`SERVICE_UNAVAILABLE` rather than searching past requirements it may state;
retry the request.

**Built-in platform adapters** (the platforms listed below) currently read one
closed age range from the brief:

```text theme={null}
Build an awareness campaign for people ages 21-35 in the United States.
```

The adapter copies the two numbers from the brief. No path invents an age
range, translates a generation label such as “Gen Z” into ages, or widens the
request to fit a platform.

This applies during product discovery and media-buy creation. It does not
change targeting on an existing media buy.

## Platform support

Each storefront must prove that it can execute the requested range exactly.

| Platform | Supported requests |
| - | - |
| Meta | Any closed range from 18 through 64. Meta's provider value `65` means 65+, so it is not an exact maximum. |
| Spotify | Any closed range from 18 through 65. |
| Google Ads | Exact unions of the native 18-24, 25-34, 35-44, 45-54, and 55-64 buckets on the supported Search products. The 65+ bucket is open-ended. |
| Snap | Exact unions of the native 18-20, 21-24, and 25-34 buckets. Open-ended buckets are not treated as a closed maximum. |
| TikTok | Exact unions of the native 18-24, 25-34, 35-44, and 45-54 buckets. The 55+ bucket is open-ended. |
| Pinterest | Exact unions of the native 18-24, 25-34, 35-44, 45-54, and 55-64 buckets. The 65+ bucket is open-ended. |
| Other direct adapters | Exact age targeting is not currently supported unless listed above. Ad-server-backed sellers may expose exact mapped age signals as described below. |

`ages 21-35` therefore works on Meta and Spotify. `ages 25-44` also works on
Google Ads because it is the exact union of two closed native buckets. A range
that cuts through a bucket, or a closed range such as `65-99` projected onto
Google's open-ended `65+` bucket, is never approximated.

## Snap: Australia minimum age

Snap prohibits reaching people under 16 when a media buy targets Australia
exclusively. If a Snap media buy targets Australia only and the brief has not
requested an age range, Apostra automatically applies an 18+ audience
before sending the buy to Snap — so if you target Australia and don't say
18+, Snap will get an 18+ audience. This only applies when Australia is the
sole targeted country; combining Australia with another country does not
trigger it.

If you explicitly request `ages 13-17` on an Australia-only Snap buy, the
media buy is rejected before it reaches Snap rather than having that request
silently overridden.

## Requests that need clarification

A Storefront-built or Agent-supplied storefront rejects a structured age it
cannot read exactly, with `field: "targeting_overlay.demographics.age"`:

* `INVALID_REQUEST` when `include_unknown` is missing, a bound is not a whole
  number, no bound is given, `min` is below 18, `min` is greater than `max`, or
  `max` is above 120.
* `UNSUPPORTED_FEATURE` when one bound is omitted (an open-ended range such as
  21+), or when `accepted_bases` or `accepted_verification_methods` is sent.

A built-in adapter asks for clarification on an ambiguous, excluded, or
open-ended age range in the brief. An exact but unsupported age range receives a
**counter-pitch** instead: a composition
storefront returns products that fit the rest of the brief and includes an
additive `limitations` entry. The proposal is composed from inventory without
age targeting; fixed-age inventory is excluded rather than silently delivering
to a different age audience. Its `message` says this explicitly, `requested`
preserves the requested range, `excluded_ranges` lists fixed ranges omitted
from the counter-pitch, and `supported_ranges` lists the exact ranges the seller
can execute (or is empty when none are declared). The storefront never
broadens, approximates, or inverts the age request.

```json theme={null}
{
  "limitations": [{
    "code": "age_targeting_unavailable",
    "message": "Requested age targeting 35-64 is not available from this Seller; proposal composed from inventory without age targeting. Fixed age ranges excluded: 25-54. Supported ranges: 25-54.",
    "requested": { "min": 35, "max": 64, "include_unknown": false },
    "excluded_ranges": [{ "min": 25, "max": 54, "include_unknown": false }],
    "supported_ranges": [{ "min": 25, "max": 54, "include_unknown": false }]
  }]
}
```

For a built-in adapter, rewrite the brief as one explicit, closed, supported
adult range when it contains:

* An open endpoint, such as `ages 21+`, `under 35`, or `up to 35`.
* More than one or a disjoint range, such as `ages 21-24 or 35-44`.
* An exclusion, such as `exclude people ages 21-35`.
* A minimum below 18.
* Conflicting or ambiguous bounds.
* A descriptive label without numbers, such as `young adults` or `Gen Z`.

This fail-closed response is expected. It prevents a storefront or model from
silently choosing a different audience.

## Unknown ages on Meta and Google Ads

Meta and Google Ads can deliver to people whose age is unknown. A
constrained-age request excludes them unless it explicitly says to include
them. In the structured field, set `include_unknown: true`. In a brief, say so
directly:

```text theme={null}
Reach people ages 21-35 and include people whose age is unknown.
```

You can also state that they must be excluded. The choice is stored with the
product, written to the provider targeting object, and checked on readback.

Meta may omit the unknown-age field when its effective value is false. In that
case, readback still preserves your explicit exclusion under the requested
controls and reports an exact match. A provider value of true never satisfies
an exclusion or an age-constrained request that did not opt in.

On Google Ads, every constrained request writes positive criteria for the
selected age buckets and negative criteria for every omitted bucket. This
includes a negative `AGE_RANGE_UNDETERMINED` criterion unless the brief
explicitly includes unknown ages; an include-unknown choice instead writes it
as a positive criterion. Exact readback verifies the complete positive and
negative set. Other platforms reject an include-unknown request unless their
row above says otherwise.

## Read the applied targeting

A returned product with verified age targeting includes the applied range at:

```json theme={null}
{
  "ext": {
    "scope3_product_targeting": {
      "demographics": {
        "age": {
          "min": 21,
          "max": 35,
          "includeUnknown": false,
          "determinationMethod": "user_verified",
          "dataProvider": "Publisher registration data"
        }
      }
    }
  }
}
```

Use the returned product for media-buy creation. The storefront checks the stored
product declaration again before publishing it or writing provider state. On
Meta, requested and provider controls also show whether unknown ages were
included.

<Note>
  **Signal-based path:** Age targeting through product-level signals
  (described in this guide) is carried by the selected product via
  `ext.scope3_product_targeting.demographics`. It is not a
  `targeting_overlay` field; the paths are separate.

  **AdCP 3.2 overlay path:** `overlaySupport.demographics` is the permission
  signal for demographic overlays, and `demographicTargeting` is the companion
  product-scoped compiler contract for supported age bounds and unknown-age
  handling. `save_media_buy` rejects `targetingOverlay.demographics` before
  dispatch while the application seller transport remains on AdCP 3.1. Treat
  these fields as
  discovery/readback data for now, and use signal targeting or omit
  demographics when staging a media buy.
</Note>

## Seller impact

Sellers using a built-in adapter do not need a new configuration field. A
custom or upstream sales agent must return the exact
`ext.scope3_product_targeting.demographics.age` declaration or its product is
filtered from that brief. As an alternative, an ad-server-backed seller can map
existing ad-server targeting to a first-class age signal:

The product declaration does not have to come from a user-level age signal. A
wholesale product can declare that its inventory itself represents the exact
age cohort—for example, from survey evidence or modeled audience composition—by
including `determinationMethod` and the responsible `dataProvider` beside its
age range. Chef can then select that inventory directly without attaching an
age signal. Descriptive copy such as “great for young adults” is not enough;
the structured bounds and provenance must be present.

```json theme={null}
{
  "signalId": "age_25_54",
  "name": "Adults 25-54",
  "signalType": "age",
  "valueType": "numeric",
  "targetingDimension": "age",
  "range": { "min": 25, "max": 54 },
  "ageDetermination": "user_provided",
  "dataProvider": "Publisher registration data",
  "adapterConfig": {
    "kind": "custom_key_value"
  }
}
```

Start from the selected ad-server targeting candidate's complete
`adapterConfig`; the abbreviated object above only illustrates the age fields.
The age meaning comes exclusively from the `age` signal type and its `range`.
A signal named “Adults 25-54” without those fields is not treated as an age
signal. Signal Manager supports three semantic types:

* `generic` is ordinary buyer-selectable targeting.
* `age` is buyer-selectable targeting with a required closed adult range and
  determination method.
* `property` is inventory addressing only and never enters the buyer-selectable
  signal catalog.

Every age signal also declares one determination method:

* `survey_based`: the person reported their age in a survey.
* `assumptive`: the provider inferred or modeled the age.
* `user_provided`: the person supplied their age, without identity verification.
* `user_verified`: the supplied age was verified against an identity source.

`dataProvider` separately names who supplied the dataset. It may name the
seller, a first-party registration system, or a third-party data provider; the
provider name does not substitute for the determination method.

When a request asks for the same closed range, the storefront can select products
from that signal's source and automatically applies the signal on the buy. A
different or partially overlapping bucket is not accepted, and signal-backed
age buckets do not include unknown ages.

Product declarations and mapped age signals describe targeting that will be
applied, not the full range the source might be capable of creating. For
example, a product or signal declaring ages 18-65 does not prove that ages
25-54 will be applied. The source must return a product or explicit signal
declaring the requested range exactly.

## Restrict acceptable age determination

Until AdCP 3.2 supplies native provenance fields, a buyer can add the temporary
Apostra extension below to the same `get_products` request:

```json theme={null}
{
  "brief": "Premium display inventory.",
  "targeting_overlay": {
    "demographics": {
      "age": { "min": 25, "max": 54, "include_unknown": false }
    }
  },
  "ext": {
    "scope3_age_targeting": {
      "accepted_determination_methods": [
        "user_provided",
        "user_verified"
      ]
    }
  }
}
```

When this allow-list is present, the storefront only returns a product whose
exact age mapping declares one of those methods. Missing, unknown, or disallowed
provenance fails closed; it is never treated as approval. A product with age
targeting baked into its inventory metadata declares the same fact as
`ext.scope3_product_targeting.demographics.age.determinationMethod`.


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