Skip to content
Open
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/flexible-availability-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add flexible-window availability discovery. `offer_filters.availability_horizon` lets a buyer ask "which dates can I run?" instead of filtering to one exact flight; sellers answer by partitioning the horizon into `time`-dimensioned forecast points (new `forecast-dimension-time` variant) carrying the new `availability_status` field (`available` | `unavailable`). Availability data is a snapshot bounded by the forecast's `valid_until`, never a hold — proposal finalization remains the firm-avails and commitment boundary. Forecast data is excluded from `list_products` feed-version scoping, and a `list_products` request whose `fields` includes `forecast` must not be answered with `outcome: "unchanged"`.
70 changes: 68 additions & 2 deletions docs/media-buy/product-discovery/media-products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1251,7 +1251,7 @@ This lets sellers forecast both delivery ("1.2M impressions") and outcomes ("1,8

Each metric value is a ForecastRange object. Provide `mid` for a point estimate, `low` and `high` for a range, or all three. At minimum, either `mid` or both `low` and `high` must be present.

Forecast points can also carry `dimensions` when a seller needs to expose availability by country, region, placement, device, audience, signal value, or an intersection such as placement x country or product x signal. `dimensions` is an array; each item declares one `kind` (`geo`, `placement`, `device_type`, `device_platform`, `audience`, or `signal`) and uses the same canonical identifiers as targeting and delivery reporting, such as `geo_level`/`geo_code` for geography, `placement_ref` for placements, and `signal_ref` plus `signal_value`/`presence` for signal buckets. Metro rows use `system` values from `metro-system` (`nielsen_dma`, `uk_itl1`, `uk_itl2`, `eurostat_nuts2`, `custom`); native postal rows use `country` plus country-local `system` values from `postal-system` (for example, `US` / `zip`, `GB` / `outward`, `ZA` / `postal_code`), while deprecated country-fused postal systems such as `us_zip` remain accepted for compatibility; country and region rows omit `system`. When multiple dimension items appear on one point, the point represents the intersection of those constraints. Dimension order has no meaning; buyers normalize row identity from `(forecast_range_unit, budget if present, product_id if present, dimensions sorted by kind)`. Sellers MUST NOT repeat the same `kind` on one point. If a seller needs multiple geo, placement, audience, or signal slices, it should emit multiple points instead. This keeps dimensional availability inside one product or proposal instead of forcing sellers to create separate products for every country, placement, or signal value. Dimension rows are independent of `pricing_options`; the product's pricing options still describe how the product is bought.
Forecast points can also carry `dimensions` when a seller needs to expose availability by country, region, placement, device, audience, signal value, or an intersection such as placement x country or product x signal. `dimensions` is an array; each item declares one `kind` (`geo`, `placement`, `device_type`, `device_platform`, `audience`, `signal`, or `time`) and uses the same canonical identifiers as targeting and delivery reporting, such as `geo_level`/`geo_code` for geography, `placement_ref` for placements, and `signal_ref` plus `signal_value`/`presence` for signal buckets. Metro rows use `system` values from `metro-system` (`nielsen_dma`, `uk_itl1`, `uk_itl2`, `eurostat_nuts2`, `custom`); native postal rows use `country` plus country-local `system` values from `postal-system` (for example, `US` / `zip`, `GB` / `outward`, `ZA` / `postal_code`), while deprecated country-fused postal systems such as `us_zip` remain accepted for compatibility; country and region rows omit `system`. When multiple dimension items appear on one point, the point represents the intersection of those constraints. Dimension order has no meaning; buyers normalize row identity from `(forecast_range_unit, budget if present, product_id if present, dimensions sorted by kind)`. Sellers MUST NOT repeat the same `kind` on one point. If a seller needs multiple geo, placement, audience, or signal slices, it should emit multiple points instead. This keeps dimensional availability inside one product or proposal instead of forcing sellers to create separate products for every country, placement, or signal value. Dimension rows are independent of `pricing_options`; the product's pricing options still describe how the product is bought.

Forecast points may include measurement-aware forecasts alongside standard delivery metrics:
- **`viewability`** mirrors the `get_media_buy_delivery` `viewability` block, but numeric values use ForecastRange objects. Sellers SHOULD only emit forecast viewability when the product can report corresponding delivery viewability. Forecast `standard` is required whenever forecast viewability values are present because MRC and GroupM rows are not interchangeable; delivery `viewability.standard` remains optional for 3.x compatibility but SHOULD be populated.
Expand All @@ -1262,7 +1262,7 @@ Forecast points may include measurement-aware forecasts alongside standard deliv
The `forecast_range_unit` field tells consumers how to interpret the points array — what axis the curve represents:

- **`spend`** (default) — points at ascending budget levels. Standard budget curve.
- **`availability`** — each point represents total available inventory for the requested targeting and dates. Budget is omitted; use `metrics.spend` to express estimated cost. Typical for guaranteed and direct-sold inventory.
- **`availability`** — each point represents total available inventory for the requested targeting and dates. Budget is omitted; use `metrics.spend` to express estimated cost. Typical for guaranteed and direct-sold inventory. With `offer_filters.availability_horizon`, points carry a `time` dimension and `availability_status` so one forecast also answers "which dates can I run?" (see [Flexible Availability Windows](#flexible-availability-windows)).
- **`reach_freq`** — points at ascending reach/frequency targets. Used in broadcast planning where the publisher shows how cost scales with frequency goals.
- **`weekly`** / **`daily`** — metrics are per-period values. Budget refers to total campaign spend. A frequency of 3.2 with `weekly` means 3.2 exposures per week.
- **`clicks`** / **`conversions`** — points at ascending outcome targets. Used in goal-based planning (e.g., "tell me your conversion goal, I'll tell you the budget").
Expand Down Expand Up @@ -1355,6 +1355,72 @@ For guaranteed and direct-sold inventory, the forecast is an availability check

The buyer agent can compare available impressions against budget requirements to identify underdelivery. If the buyer needs 500,000 impressions at a \$20 CPM to spend their full \$10K budget, and the forecast shows 400,000 mid available, the buyer knows \$2K of budget must be allocated elsewhere.

#### Flexible Availability Windows

Exact-date and flexible-date discovery are two readings of the same question. When the buyer already knows the flight, `offer_filters.start_date`/`end_date` scope discovery to it and the availability forecast answers "can I run these dates?". When the buyer is flexible — common for calendar-shaped inventory like homepage takeovers and sponsorships — `offer_filters.availability_horizon` asks the complementary question, "which dates can I run?":

```json
{
"offer_filters": {
"availability_horizon": {
"start_time": "2026-09-01T00:00:00-04:00",
"end_time": "2026-10-01T00:00:00-04:00"
}
}
}
```

The seller answers by partitioning the horizon into `time`-dimensioned forecast points, each carrying an `availability_status`:

```json
{
"forecast_range_unit": "availability",
"method": "modeled",
"currency": "USD",
"generated_at": "2026-08-18T12:00:00Z",
"valid_until": "2026-08-18T12:05:00Z",
"points": [
{
"dimensions": [
{ "kind": "time", "start_time": "2026-09-01T00:00:00-04:00", "end_time": "2026-09-20T00:00:00-04:00" }
],
"availability_status": "available",
"metrics": {
"impressions": { "mid": 20000 },
"spend": { "mid": 6400 }
}
},
{
"dimensions": [
{ "kind": "time", "start_time": "2026-09-20T00:00:00-04:00", "end_time": "2026-09-27T00:00:00-04:00" }
],
"availability_status": "unavailable",
"metrics": {}
},
{
"dimensions": [
{ "kind": "time", "start_time": "2026-09-27T00:00:00-04:00", "end_time": "2026-10-01T00:00:00-04:00" }
],
"availability_status": "available",
"metrics": {
"impressions": { "mid": 4200 },
"spend": { "mid": 1350 }
}
}
]
}
```

Window semantics:

- **Windows are half-open** (`[start_time, end_time)`), non-overlapping, and coalesced — adjacent windows with the same status and materially identical metrics are merged.
- **A complete response partitions the requested horizon.** A seller that cannot cover the full horizon signals the gap through the response's `incomplete[]` mechanism rather than silently omitting windows.
- **`availability_status` is a snapshot, never a hold.** `valid_until` bounds its freshness. Commitment happens at proposal finalization or purchase — a buy against a window that has since closed fails with `PRODUCT_UNAVAILABLE`, and the buyer re-reads. Buyers that need a guarantee take an inventory hold by finalizing a proposal rather than polling for fresher snapshots.
- **`unavailable` covers all non-bookable windows, not only held ones.** Sellers MUST apply all product booking eligibility rules — including minimum and maximum bookable duration, day-of-week restrictions, and other scheduling constraints — before reporting status. A gap that is not held by any buyer but is too short to meet the product's minimum duration is `unavailable`, not `available`. Buyers can safely skip `unavailable` rows without attempting a booking; doing so avoids an otherwise-predictable `PRODUCT_UNAVAILABLE` error.
- **No demand disclosure.** Sellers MUST NOT disclose who holds a closed window, and `unavailable` rows may carry empty metrics.
- **`availability_horizon` is not an eligibility filter.** Products remain returnable when only part of the horizon is open — that partial openness is exactly what the buyer asked to see. It is mutually exclusive with `start_date`/`end_date`; buyers that already know their flight use those instead.
- **Derived values stay buyer-side.** "Next available date" and similar conveniences are computed by the buyer from the windows; they are not response fields.

#### Dimensional Availability and Measurement

Sellers can expose availability by country, placement, signal value, placement x country, product x signal, or other dimensional intersections as forecast points on the same product. A point with multiple `dimensions` items represents the intersection of all listed constraints. Rows are comparable when they use the same dimension grain, such as placement x country rows for several countries or signal-value rows for one product baseline. Buyers MUST NOT sum rows unless the seller explicitly documents that the rows form a complete, non-overlapping partition.
Expand Down
2 changes: 2 additions & 0 deletions docs/media-buy/product-discovery/proposal-negotiation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Every transition creates a new `proposal_id`; the source snapshot remains unchan

`draft` terms are indicative and do not reserve inventory. `finalize` copies an unchanged draft into a `committed` snapshot and creates a hold until `expires_at`. [`accept_proposal`](/docs/media-buy/task-reference/accept_proposal) consumes that hold and creates, updates, or cancels the MediaBuy according to `proposal_kind`.

In traditional media terms, this lifecycle is the two-phase avails check. A draft is a **soft avail**: the seller answers from models or cached inventory state, forecasts carry `valid_until` freshness bounds, and nothing is reserved — another buyer can take the inventory before this one commits. `finalize` is where the **firm avail** runs: the seller hits its inventory system, re-prices if needed, and either takes the hold or fails the finalize. Draft numbers are therefore non-binding, and a finalize returning different pricing or forecast values than its source draft showed is correct behavior, not an error. Buyers that need certainty across a slow approval chain hold inventory by finalizing early — not by polling discovery for fresher snapshots.

## Discover what the seller supports

Read `get_adcp_capabilities.media_buy.lifecycle_tools` before selecting the compact lifecycle. A negotiating seller includes `refine_proposals`; it may also publish `media_buy.proposal_refinement`:
Expand Down
12 changes: 12 additions & 0 deletions docs/media-buy/task-reference/list_products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ If the selected feed and pricing layer still match, the seller can omit `product
}
```

## Availability forecasts are a per-read overlay

For calendar-shaped guaranteed inventory (homepage takeovers, sponsorships), `list_products` can answer "which dates are open?" directly: pass `criteria.offer_filters.availability_horizon` and request `forecast` in `fields`, and each product's forecast comes back partitioned into `time`-dimensioned points carrying `availability_status`. See [Flexible Availability Windows](/docs/media-buy/product-discovery/media-products#flexible-availability-windows) for window semantics.

Availability is volatile — every booking changes it — so it is never part of the versioned feed content:

- `feed_version` covers the durable offer definition only. Forecast data (including availability windows) does not participate in feed-version scoping and carries its own freshness via the forecast's `generated_at` and `valid_until`.
- A request whose `fields` includes `forecast` MUST NOT be answered with `outcome: "unchanged"` — that arm omits `products`, so it cannot carry the volatile data the buyer explicitly asked for. Sellers return a full `listed` response even when the feed token matches.
- Wholesale feed webhooks fire on definition and pricing changes, not on booking-state changes. Broadcasting window closures to all subscribers would leak demand signals; buyers who need current availability read it on demand, and buyers who need a guarantee take an inventory hold by finalizing a proposal.

In practice these are two different reads: mirror maintenance (conditional, no `forecast` in `fields`) and availability checks (`forecast` requested, never conditional). Any `availability_status` returned is a snapshot bounded by `valid_until`, never a hold — the buy itself remains the authoritative availability check and fails with [`PRODUCT_UNAVAILABLE`](/docs/building/verification/compliance-catalog#error-code-product-unavailable) when a window has since closed.

## Wholesale feed webhooks

Buyers maintaining a wholesale product mirror register `product.*` and `wholesale_feed.bulk_change` subscribers through `sync_accounts.accounts[].notification_configs[]`. Registration is account-level and durable; `push_notification_config` on an individual `list_products` request does not create that subscription.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
"test": "npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:webhook-receiver-envelope && npm run test:oauth-setup-vectors && npm run test:a2a-profile-extension && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:language-targeting && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:signal-governance-storyboard && npm run test:governance-task-slices && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck",
"test:all": "npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:error-handling && npm run test:snippets && npm run typecheck",
"precommit:server-unit": "node scripts/precommit-server-unit.cjs",
"precommit": "bash scripts/with-timeout.sh 180 npm run test:unit && npm run test:test-dynamic-imports && npm run test:format-identity-boundaries && npm run test:callapi-state-change && bash scripts/with-timeout.sh 240 npm run precommit:server-unit && npm run typecheck",
"precommit": "bash scripts/with-timeout.sh 180 npm run test:unit && npm run test:test-dynamic-imports && npm run test:format-identity-boundaries && npm run test:callapi-state-change && bash scripts/with-timeout.sh 600 npm run precommit:server-unit && npm run typecheck",
"test:storyboards": "bash scripts/run-storyboards-matrix.sh",
"test:storyboards:3.0-compat": "bash scripts/run-storyboards-3-0-compat.sh",
"prepare": "sh scripts/configure-git-hooks.sh",
Expand Down
1 change: 1 addition & 0 deletions static/schemas/source/core/canonical-forecast-point.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"budget": { "type": "number", "minimum": 0 },
"product_id": { "type": "string", "x-entity": "product" },
"dimensions": { "$ref": "/schemas/core/forecast-point-dimensions.json" },
"availability_status": { "$ref": "/schemas/enums/availability-status.json" },
"metrics": {
"type": "object",
"additionalProperties": { "$ref": "/schemas/core/forecast-range.json" }
Expand Down
Loading
Loading