diff --git a/.changeset/flexible-availability-windows.md b/.changeset/flexible-availability-windows.md new file mode 100644 index 0000000000..65ba50225f --- /dev/null +++ b/.changeset/flexible-availability-windows.md @@ -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"`. diff --git a/docs/media-buy/product-discovery/media-products.mdx b/docs/media-buy/product-discovery/media-products.mdx index f829c76327..a381e468fd 100644 --- a/docs/media-buy/product-discovery/media-products.mdx +++ b/docs/media-buy/product-discovery/media-products.mdx @@ -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. @@ -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"). @@ -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. diff --git a/docs/media-buy/product-discovery/proposal-negotiation.mdx b/docs/media-buy/product-discovery/proposal-negotiation.mdx index 4b1d75d0c0..2255b92d2f 100644 --- a/docs/media-buy/product-discovery/proposal-negotiation.mdx +++ b/docs/media-buy/product-discovery/proposal-negotiation.mdx @@ -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`: diff --git a/docs/media-buy/task-reference/list_products.mdx b/docs/media-buy/task-reference/list_products.mdx index cc79d45f78..96fab97349 100644 --- a/docs/media-buy/task-reference/list_products.mdx +++ b/docs/media-buy/task-reference/list_products.mdx @@ -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. diff --git a/package.json b/package.json index 48a05f9c94..8567ea6486 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/static/schemas/source/core/canonical-forecast-point.json b/static/schemas/source/core/canonical-forecast-point.json index d16d3a4c9f..48f1fdf8de 100644 --- a/static/schemas/source/core/canonical-forecast-point.json +++ b/static/schemas/source/core/canonical-forecast-point.json @@ -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" } diff --git a/static/schemas/source/core/forecast-dimension-time.json b/static/schemas/source/core/forecast-dimension-time.json new file mode 100644 index 0000000000..db7cc29768 --- /dev/null +++ b/static/schemas/source/core/forecast-dimension-time.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/core/forecast-dimension-time.json", + "title": "Time Forecast Dimension", + "description": "A calendar-window dimension for a ForecastPoint row. Variant of ForecastPoint dimensions; see forecast-point-dimensions.json for dispatch rules. Windows are half-open intervals [start_time, end_time): the row covers instants at or after start_time and strictly before end_time, so adjacent windows share a boundary without overlapping. end_time MUST be after start_time; JSON Schema draft-07 cannot compare sibling values, so conformance tooling enforces the ordering. Within one forecast, sellers MUST emit non-overlapping windows and SHOULD coalesce adjacent windows whose availability_status and metrics do not materially differ. When the request scoped the forecast with offer_filters.availability_horizon, a complete forecast 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.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "time", + "description": "Dimension family discriminator." + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Inclusive window start (RFC 3339 date-time with timezone offset)." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Exclusive window end (RFC 3339 date-time with timezone offset). MUST be after start_time." + } + }, + "required": ["kind", "start_time", "end_time"], + "additionalProperties": false +} diff --git a/static/schemas/source/core/forecast-point-dimensions.json b/static/schemas/source/core/forecast-point-dimensions.json index f341e672cd..d2d8fa92d1 100644 --- a/static/schemas/source/core/forecast-point-dimensions.json +++ b/static/schemas/source/core/forecast-point-dimensions.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/core/forecast-point-dimensions.json", "title": "Forecast Point Dimensions", - "description": "Dimension constraints represented by a ForecastPoint. Use this when one product, proposal, or signal coverage forecast needs to expose availability or forecasted delivery by country, region, placement, device, audience, signal value, or intersections such as placement x country without creating separate products solely for each slice. Each item declares one dimension family via `kind`; when multiple items are present, the point represents their intersection. Sellers MUST NOT emit more than one item for each `kind` in a point. Consumers MUST NOT treat repeated kinds as OR semantics; repeated peer values such as two countries are a seller conformance issue. Dimension values are descriptors of the forecast row and are independent of pricing_options.", + "description": "Dimension constraints represented by a ForecastPoint. Use this when one product, proposal, or signal coverage forecast needs to expose availability or forecasted delivery by country, region, placement, device, audience, signal value, time window, or intersections such as placement x country without creating separate products solely for each slice. Each item declares one dimension family via `kind`; when multiple items are present, the point represents their intersection. Sellers MUST NOT emit more than one item for each `kind` in a point. Consumers MUST NOT treat repeated kinds as OR semantics; repeated peer values such as two countries are a seller conformance issue. Dimension values are descriptors of the forecast row and are independent of pricing_options.", "x-adcp-validation": { "unique_item_properties": ["kind"], "description": "`dimensions[]` is an array for readable intersection syntax, but `kind` is a logical set key. JSON Schema draft-07 cannot express unique-by-property for this array shape, so conformance tooling should reject repeated `kind` values." @@ -29,6 +29,9 @@ }, { "$ref": "/schemas/core/forecast-dimension-signal.json" + }, + { + "$ref": "/schemas/core/forecast-dimension-time.json" } ] } diff --git a/static/schemas/source/core/forecast-point.json b/static/schemas/source/core/forecast-point.json index 882b986645..c3a5f61c1b 100644 --- a/static/schemas/source/core/forecast-point.json +++ b/static/schemas/source/core/forecast-point.json @@ -23,7 +23,11 @@ }, "dimensions": { "$ref": "/schemas/core/forecast-point-dimensions.json", - "description": "Dimension constraints represented by this forecast point, such as country, region, placement, device type, platform, audience, signal value, or intersections such as placement x country or product x signal. Each item declares one dimension family; when multiple items are present, the point represents their intersection. Sellers MUST NOT emit more than one item for each `kind` on a point; consumers MUST NOT treat repeated kinds as OR semantics. Use multiple points with dimensions to expose country/placement/signal availability within one product, proposal, or signal coverage forecast without creating separate products solely for each dimension. Dimensions describe the forecast row and are independent of pricing_options." + "description": "Dimension constraints represented by this forecast point, such as country, region, placement, device type, platform, audience, signal value, time window, or intersections such as placement x country or product x signal. Each item declares one dimension family; when multiple items are present, the point represents their intersection. Sellers MUST NOT emit more than one item for each `kind` on a point; consumers MUST NOT treat repeated kinds as OR semantics. Use multiple points with dimensions to expose country/placement/signal availability within one product, proposal, or signal coverage forecast without creating separate products solely for each dimension. Dimensions describe the forecast row and are independent of pricing_options." + }, + "availability_status": { + "$ref": "/schemas/enums/availability-status.json", + "description": "Bookability of the inventory this row describes, as of the forecast's generated_at. Most meaningful on rows with a time dimension in an availability forecast (forecast_range_unit 'availability'). This is a snapshot, not a hold: valid_until bounds freshness, and proposal finalization or purchase remains the commitment boundary. When omitted, the row makes no bookability claim. 'unavailable' rows may carry empty metrics." }, "metrics": { "type": "object", diff --git a/static/schemas/source/core/product-offer-filters.json b/static/schemas/source/core/product-offer-filters.json index 0cf226c096..d7cc007a14 100644 --- a/static/schemas/source/core/product-offer-filters.json +++ b/static/schemas/source/core/product-offer-filters.json @@ -41,8 +41,34 @@ }, "standard_formats_only": { "type": "boolean" }, "min_exposures": { "type": "integer", "minimum": 1 }, - "start_date": { "type": "string", "format": "date" }, - "end_date": { "type": "string", "format": "date" }, + "start_date": { + "type": "string", + "format": "date", + "description": "Fixed-flight availability filter: with end_date, declares the exact flight the buyer intends to run. Returned products MUST be able to serve that flight, and pricing and forecasts are scoped to it. Mutually exclusive with availability_horizon." + }, + "end_date": { + "type": "string", + "format": "date", + "description": "Fixed-flight availability filter end. See start_date. Mutually exclusive with availability_horizon." + }, + "availability_horizon": { + "type": "object", + "description": "Flexible-window availability discovery: the buyer is open to any bookable window inside [start_time, end_time) and asks the seller to describe when the returned inventory can run, instead of filtering to one exact flight. Sellers that support this field partition the horizon into time-dimensioned forecast rows (forecast-dimension-time) carrying availability_status; sellers that cannot cover the full horizon signal the gap via the response's incomplete[] mechanism. Unlike start_date/end_date this is not an eligibility filter — products remain returnable when only part of the horizon is open. The resulting availability is a snapshot bounded by the forecast's valid_until, never a hold. Mutually exclusive with start_date and end_date, which declare a fixed flight; buyers that already know their dates use those instead.", + "properties": { + "start_time": { + "type": "string", + "format": "date-time", + "description": "Inclusive horizon start (RFC 3339 date-time with timezone offset)." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Exclusive horizon end (RFC 3339 date-time with timezone offset). MUST be after start_time." + } + }, + "required": ["start_time", "end_time"], + "additionalProperties": false + }, "budget_range": { "$ref": "/schemas/core/budget-range.json" }, @@ -161,5 +187,15 @@ }, "ext": { "$ref": "/schemas/core/ext.json" } }, + "allOf": [ + { + "not": { + "anyOf": [ + { "required": ["availability_horizon", "start_date"] }, + { "required": ["availability_horizon", "end_date"] } + ] + } + } + ], "additionalProperties": false } diff --git a/static/schemas/source/enums/availability-status.json b/static/schemas/source/enums/availability-status.json new file mode 100644 index 0000000000..c543c2dd94 --- /dev/null +++ b/static/schemas/source/enums/availability-status.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/enums/availability-status.json", + "title": "Availability Status", + "description": "Bookability of the inventory a forecast row describes, as of the forecast's generated_at. A snapshot, never a hold: valid_until bounds freshness, and proposal finalization or purchase remains the commitment boundary.", + "type": "string", + "enum": ["available", "unavailable"], + "enumDescriptions": { + "available": "The inventory described by this row can be booked as of generated_at. This is a snapshot, not a reservation — another buyer can take the window before this buyer commits.", + "unavailable": "The inventory described by this row cannot be booked. Sellers MUST apply all product booking eligibility constraints — including minimum and maximum bookable duration, day-of-week restrictions, and other scheduling rules — before reporting status; a window that fails any such constraint is 'unavailable' even when no competing hold exists. Rows may carry empty metrics. Sellers MUST NOT disclose who holds a conflicting booking." + } +} diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json index 795f0bb017..c6cb1d0239 100644 --- a/static/schemas/source/index.json +++ b/static/schemas/source/index.json @@ -486,6 +486,10 @@ "$ref": "/schemas/core/forecast-dimension-signal.json", "description": "Signal forecast dimension variant" }, + "forecast-dimension-time": { + "$ref": "/schemas/core/forecast-dimension-time.json", + "description": "Calendar-window forecast dimension variant for availability windows" + }, "forecast-vendor-metric-value": { "$ref": "/schemas/core/forecast-vendor-metric-value.json", "description": "Forecasted vendor-defined measurement value with low/mid/high bounds" @@ -1177,6 +1181,10 @@ "$ref": "/schemas/enums/forecast-range-unit.json", "description": "How to interpret forecast points: spend curve, reach/frequency curve, temporal (weekly/daily), or outcome targets (clicks/conversions)" }, + "availability-status": { + "$ref": "/schemas/enums/availability-status.json", + "description": "Bookability of the inventory a forecast row describes (available, unavailable)" + }, "demographic-system": { "$ref": "/schemas/enums/demographic-system.json", "description": "Audience measurement systems for demographic notation (nielsen, barb, agf, oztam, mediametrie, custom)"