Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-placement-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Unify package targeting intent and applied readback. Placement selection now lives inside `targeting_overlay`, properties/placements/collections resolve jointly through `targeting_resolution.inventory`, demographic execution moves under `targeting_resolution.demographics`, and all targeting axes share a complete applied overlay with exact-equivalence semantics. Adds the `update_placements` lifecycle action and `PLACEMENT_SELECTION_INVALID` recovery contract.
43 changes: 27 additions & 16 deletions docs/media-buy/advanced-topics/targeting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,11 @@ Multiple intervals or signals may be unioned only when their canonical predicate

Age is a registered restricted-attribute category because some campaign plans prohibit its use in regulated contexts. A direct `targeting_overlay.demographics.age` predicate and a signal carrying `demographic_predicate.age` are both age-based targeting for governance evaluation. They are blocked only when the applicable campaign plan lists `age` in `restricted_attributes`; the registry entry does not create a global prohibition on otherwise lawful demographic targeting.

### Exact readback
### Exact targeting readback

Whenever demographic targeting was requested or applied, package state includes `demographic_targeting_resolution`. It preserves both predicates and the execution mechanism:
Package state separates buyer intent from seller-applied targeting. `targeting_overlay` preserves the complete request; `targeting_resolution.applied` reports the complete applied result in the same vocabulary. `equivalent` is always `true` for stored packages—sellers reject silent drift. Specialized evidence lives under the same resolution object: `demographics` records predicate execution, while `inventory` preserves property-placement-collection relationships.

Whenever demographic targeting was requested or applied, `targeting_resolution.demographics` preserves both predicates and the execution mechanism:

```json
{
Expand All @@ -169,27 +171,36 @@ Whenever demographic targeting was requested or applied, package state includes
}
}
},
"demographic_targeting_resolution": {
"requested": {
"age": {
"min": 21,
"include_unknown": false,
"accepted_bases": ["verified", "declared"],
"accepted_verification_methods": ["world_id"]
}
},
"targeting_resolution": {
"applied": {
"age": { "min": 21, "include_unknown": false }
"demographics": {
"age": { "min": 21, "include_unknown": false }
}
},
"equivalent": true,
"execution": { "type": "continuous_bounds" },
"applied_bases": ["verified"],
"applied_verification_methods": ["world_id"]
"resolved_at": "2026-09-01T10:00:00Z",
"demographics": {
"requested": {
"age": {
"min": 21,
"include_unknown": false,
"accepted_bases": ["verified", "declared"],
"accepted_verification_methods": ["world_id"]
}
},
"applied": {
"age": { "min": 21, "include_unknown": false }
},
"equivalent": true,
"execution": { "type": "continuous_bounds" },
"applied_bases": ["verified"],
"applied_verification_methods": ["world_id"]
}
}
}
```

`targeting_overlay.demographics` preserves the booked predicate and buyer determination constraints. The resolution's `applied` field contains the canonical predicate actually applied; `equivalent` is always `true` for stored packages, and buyers should still recompute predicate equality from `requested` and `applied`. `applied_bases` and `applied_verification_methods` record the effective configured eligibility paths after intersecting buyer constraints, product capability, and age compliance. They do not assert which path every individual impression used. A seller rejects any non-equivalent or unsupported compilation. The same core `Package` schema carries this readback in synchronous `CreateMediaBuySuccess` responses and terminal async completion artifacts; `get_media_buys` exposes the corresponding `PackageStatus` field. Updates use the normal full-overlay replacement semantics and refresh the resolution atomically.
`targeting_overlay.demographics` preserves the requested predicate and buyer determination constraints. `targeting_resolution.applied.demographics` is the canonical applied overlay value, while `targeting_resolution.demographics` carries the lossless execution proof. Buyers should recompute predicate equality from its `requested` and `applied` values. `applied_bases` and `applied_verification_methods` record effective configured eligibility paths; they do not assert which path every impression used. The same Package shape appears on create, update, async completion, and `get_media_buys`. Updates replace the requested overlay and refresh the entire resolution atomically.

## Why Brief-Based Targeting?

Expand Down
5 changes: 3 additions & 2 deletions docs/media-buy/product-discovery/media-products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Products can optionally declare specific public ad placements within their inven
- **`kind: "seller_inline"`** - Public buyer-facing placement metadata defined inline by the sales agent; requires `name`
- **`publisher_domain`** - Domain whose `adagents.json` defines the referenced placement. New multi-publisher products SHOULD include it so the placement namespace is explicit.
- **`placement_id`** - Placement ID in the publisher namespace. Buyers reference it with `publisher_domain` in `creative_assignments[].placement_refs`; legacy `placement_ids` strings are only unambiguous in single-publisher contexts.
- **`mode: "targetable"`** - The buyer may reference this publisher-scoped placement when assigning creatives or otherwise selecting placements within the product
- **`mode: "targetable"`** - The buyer may select this publisher-scoped placement in `packages[].targeting_overlay.placement_selection` and route creatives to it with `creative_assignments[].placement_refs`
- **`mode: "included"`** - The public placement is part of the product's described composition, but the buyer cannot cherry-pick it by `placement_id`
- **`video_placement_types`** - Declared video placement types for OLV and other video placements. Concrete placements usually declare one value; aggregate placements may declare multiple.
- **`audio_distribution_types`** - Declared audio distribution types for radio, streaming-audio, podcast, gaming, and other audio placements. Concrete placements usually declare one value; aggregate placements may declare multiple.
Expand All @@ -205,6 +205,7 @@ Products can optionally declare specific public ad placements within their inven
- **Publisher reference rule** - Publisher-referenced product placements resolve to `{publisher_domain, placement_id}` in the publisher's `adagents.json`
- **Private inventory rule** - Seller-private delivery objects, ad-server mappings, and source/origin details must stay out of `get_products`
- **Creative assignment** - Different creatives can be assigned to targetable placements
- **Purchased placement selection** - `packages[].targeting_overlay.placement_selection` chooses the complete purchased targetable-placement set and is resolved jointly with property and collection targeting; `mode: "included"` placements remain included
- **Omitting placement targeting** - Creatives without `placement_refs` or legacy `placement_ids` run on all buyer-targetable placements in the package, and the seller still controls included-only delivery composition
- **Use registered IDs when available** - If the publisher declares canonical `placements` in `adagents.json`, product placements SHOULD use the catalog ID as `placement_id`
- **Preserve registry semantics** - When a product references a registered placement, it is referring to that same placement. The product may narrow `format_ids` or `format_options`, or add operational detail, but it should not change the placement's meaning incompatibly
Expand Down Expand Up @@ -1252,7 +1253,7 @@ A buyer agent turns a chosen dimensional row into a buy through the existing buy
- `kind: "geo"` rows map to `packages[].targeting_overlay.geo_countries`, `geo_regions`, `geo_metros`, or `geo_postal_areas`, depending on `geo_level` and seller support. Country rows use ISO 3166-1 alpha-2 `geo_code`; region rows use ISO 3166-2 `geo_code`; metro rows include the corresponding targeting `system` enum; native postal rows include `country` plus the country-local `system`.
- `kind: "device_type"` and `kind: "device_platform"` rows map to the corresponding targeting overlay fields when the seller supports device targeting.
- `kind: "audience"` rows map to `audience_include` or signal targeting only when the audience is selectable for that product; informational audience rows are planning signals, not automatic targeting handles.
- `kind: "placement"` rows map first to product refinement or seller-supported placement targeting for the package. `dimensions[].placement_ref` identifies the inventory slice the forecast row describes; it does not by itself narrow the purchased package and buyers SHOULD NOT treat it as a shortcut for `creative_assignments[].placement_refs`. If the buyer wants to buy only that placement, the product must expose the placement as `mode: "targetable"` or the buyer should request a refined product/proposal. `creative_assignments[].placement_refs` is only the creative-routing surface after the buy's inventory scope is established; it does not by itself narrow the purchased inventory. Proposal-level forecast points with placement dimensions should include `product_id` when the placement maps to one allocation's product; without product context, placement rows on proposal-level forecasts are informational planning rows, not directly executable choices.
- `kind: "placement"` rows map to `packages[].targeting_overlay.placement_selection` when the product exposes the referenced placement as `mode: "targetable"`; otherwise the buyer requests a refined product/proposal. `dimensions[].placement_ref` identifies the inventory slice the forecast row describes but does not by itself narrow the purchased package. Buyers SHOULD NOT treat it as a shortcut for `creative_assignments[].placement_refs`, which remains only the creative-routing surface within the purchased selection. Proposal-level forecast points with placement dimensions should include `product_id` when the placement maps to one allocation's product; without product context, placement rows on proposal-level forecasts are informational planning rows, not directly executable choices.

`kind: "signal"` rows use canonical `signal_ref` plus optional `signal_value` to describe a signal bucket. Use `presence: "present"` for rows where the signal is available with the supplied value, and `presence: "absent"` with `signal_value: null` for the explicit not-present bucket. `signal_id` is only a shorthand when the enclosing object already identifies the signal unambiguously, such as a coverage forecast nested directly under a single `get_signals` item. Product-level forecasts use `ForecastPoint.product_id` for product context; do not add a separate product dimension item.

Expand Down
8 changes: 6 additions & 2 deletions docs/media-buy/task-reference/create_media_buy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ When executing a proposal, `proposal_status` on the returned proposal determines
| `pacing` | string | No | `"even"` (default), `"asap"`, or `"front_loaded"` |
| `bid_price` | number | No | Bid price for auction pricing. This is the exact bid/price to honor unless the selected pricing option has `max_bid: true`, in which case it is treated as the buyer's maximum willingness to pay (ceiling). |
| `optimization_goals` | [OptimizationGoal[]](/docs/media-buy/conversion-tracking/#optimization-goals) | No | Optimization targets for this package. Each goal is either `kind: "event"` (conversion events with `event_sources` array, optional `cost_per`, `per_ad_spend`, or `maximize_value` target) or `kind: "metric"` (seller-native metric with optional `cost_per` or `threshold_rate` target). Event goals require `conversion_tracking.supported_targets` on the product; metric goals require `metric_optimization.supported_metrics`. |
| `targeting_overlay` | TargetingOverlay | No | Additional targeting criteria (see [Targeting](/docs/media-buy/advanced-topics/targeting)). For `demographics`, inspect the selected product's `demographic_targeting` declaration first; sellers accept only an exact compilation or reject it. A buyer choosing a suggested supported predicate submits a new request; sellers never silently broaden or narrow the original. |
| `targeting_overlay` | TargetingOverlay | No | Complete buyer-requested targeting (see [Targeting](/docs/media-buy/advanced-topics/targeting)). Property, placement, and collection inventory axes are resolved jointly. Use `placement_selection: {mode: "selected", placement_refs: [...]}` for the complete selected targetable-placement set or `{mode: "default"}` for the product/seller default. For `demographics`, inspect the selected product's `demographic_targeting` declaration first; sellers accept only an exact compilation or reject it. Sellers never silently broaden, narrow, drop, or substitute any targeting axis. |
| `start_time` | string | No | ISO 8601 date-time for this package's flight start. When omitted, inherits the media buy's `start_time`. Must fall within the media buy's date range. Does not support `"asap"`. |
| `end_time` | string | No | ISO 8601 date-time for this package's flight end. When omitted, inherits the media buy's `end_time`. Must fall within the media buy's date range. |
| `creative_assignments` | CreativeAssignment[] | No | Assign existing library creatives with optional weights and placement targeting |
Expand All @@ -208,6 +208,8 @@ When executing a proposal, `proposal_status` on the returned proposal determines
| `performance_standards` | [PerformanceStandard[]](/docs/media-buy/advanced-topics/pricing-models#measurement-terms-and-performance-standards) | No | Buyer's proposed performance standards (viewability, IVT, completion rate, brand safety, attention score). Overrides product defaults. Seller accepts, rejects with `TERMS_REJECTED`, or adjusts. When omitted, product's `performance_standards` apply. |
| `committed_metrics` | object[] | No | Buyer's proposed reporting contract — metrics the buyer wants the seller to commit to populating in delivery reports. Same negotiation pattern as `measurement_terms`/`performance_standards`: each entry tags `scope: "standard"` (with `metric_id` from the closed enum) or `scope: "vendor"` (with `vendor` BrandRef + vendor's `metric_id`). Request-side entries do NOT carry `committed_at` — that timestamp is stamped by the seller on accept. Seller accepts (echoes on response with `committed_at`), rejects with `TERMS_REJECTED`, or normalizes (echoes a different but compatible list). When omitted, the seller decides what to commit based on the product's `available_metrics` plus any `required_metrics` filter the buyer passed at discovery. |

`targeting_overlay.placement_selection` controls purchased placement inventory, while `creative_assignments[].placement_refs` only routes a creative within that inventory. Product placements with `mode: "included"` remain included and cannot be removed by the buyer. Sellers resolve properties, placements, and collections as one inventory graph: every explicitly selected placement must have at least one eligible property-placement pair after property targeting. Unknown, cross-publisher, duplicate, included-only, property-incompatible, collection-incompatible, or otherwise invalid selections are rejected with `PLACEMENT_SELECTION_INVALID`. A successful create echoes the exact requested `targeting_overlay` and returns the complete applied result in `targeting_resolution`; its `inventory.placements[].property_scope` preserves where each placement can actually run.

## Response

### Success Response
Expand All @@ -219,7 +221,7 @@ When executing a proposal, `proposal_status` on the returned proposal determines
| `confirmed_at` | ISO 8601 timestamp when the seller committed to the media buy. Stable after it is set. May be `null` in deferred/manual approval flows until seller commitment occurs. |
| `creative_deadline` | ISO 8601 timestamp for creative upload deadline |
| `revision` | Initial media-buy revision. Use this value as the `revision` token on the next `update_media_buy` call intended to change state. |
| `packages` | Array of created packages with complete state. Packages may include per-package `creative_deadline` when different from the media buy deadline, and SHOULD echo every format selector field supplied on create (`format_option_refs`, `format_ids`, and/or `format_kind`/`params`) so read surfaces are lossless even when one selector wins precedence. |
| `packages` | Array of created packages with complete state. Packages may include per-package `creative_deadline`, SHOULD echo every supplied format selector, and MUST echo the requested `targeting_overlay` plus seller-confirmed `targeting_resolution` whenever targeting was requested. |

`confirmed_at` is seller commitment time, not a delivery-status timestamp. Do not update it when a buy later pauses, resumes, starts delivery, completes, or reports performance. A committed synchronous create stamps it immediately. Use the `submitted` response branch when no `media_buy_id` is being returned to the buyer. Sellers MAY instead return synchronous success with `media_buy_id`, `packages`, and `confirmed_at: null` for a provisional buy; such buys MUST be retrievable via `get_media_buys` and MUST transition by setting `confirmed_at` exactly once on commitment. A provisional buy with `confirmed_at: null` MUST NOT be `active` and MUST NOT include `packages[].committed_metrics`.

Expand Down Expand Up @@ -1023,6 +1025,8 @@ Common errors and resolutions:
| `TARGETING_TOO_NARROW` | Targeting yields zero inventory | Broaden geographic or audience criteria |
| `POLICY_VIOLATION` | Brand/product violates policy | Review publisher's content policies |
| `INVALID_PRICING_OPTION` | pricing_option_id not found | Use ID from product's `pricing_options` |
| `PLACEMENT_SELECTION_INVALID` | Placement identity, selectability, combination, or creative-routing consistency is invalid | Branch on `error.details.reason`; choose a valid complete set from the product's targetable placements or use `mode: "default"` |
| `REQUOTE_REQUIRED` | The placement set is valid but changes the product's priced envelope | Re-discover products or choose a replacement product/package with terms covering the requested placement set |
| `CREATIVE_ID_EXISTS` | Creative ID already exists in the seller's creative namespace | For library-backed sellers, assign existing creatives via `creative_assignments` or update via `sync_creatives`; for inline-only sellers, use a different package-scoped `creative_id` |

Example error response:
Expand Down
Loading
Loading