Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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-time-based-views.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add `time_based_views` to delivery reporting: an array of time-threshold video view counts, each entry keyed by (threshold_seconds, basis). The new `view-threshold-basis` enum distinguishes play-time counting (platform 2s/6s video views) from in-view counting (IAB/MRC viewable video), which are materially different numbers at the same threshold and must not be conflated or summed. Capability-gated via the `time_based_views` token in available-metric. Implements RFC #6430 with the basis discriminator the RFC's open questions pointed toward.
5 changes: 5 additions & 0 deletions .changeset/metric-leaf-identities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add leaf metric identities so nested delivery values are individually declarable, committable, aggregatable, and sortable: `quartile_25`–`quartile_100` (resolving to `quartile_data.q1_views`–`q4_views`) and `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds` (resolving to the same-named `viewability` fields) join `available-metric` and `sort-metric`. This closes an existing contradiction: `committed-metric` qualifier rules and `delivery-metric-aggregate` conditionals already referenced these metric_ids, and the shipped `committed_metrics` / `metric_aggregates` examples were invalid against their own schemas. Also adds the missing flat transactional scalars (`commissionable_value`, `plays`, `cost_per_completed_view`, `cpm`, `downloads`, `units_sold`, `new_to_brand_units`) to `sort-metric`, with survey/model-based lift scalars documented as intentionally sort-excluded. Leaf identities resolve to the nested canonical values — no duplicate flat response fields are introduced. A metric-identity coherence contract test now enforces enum/schema/example agreement.
3 changes: 3 additions & 0 deletions docs/media-buy/media-buys/optimization-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ Buyers receive the intersection of both. `impressions` and `spend` are always re
- **`profile_visits`**: Visits to the brand's in-platform page
- **`viewability`**: Viewability data (measurable_impressions, viewable_impressions, viewable_rate, viewed_seconds, standard, vendor). Separates MRC and GroupM standards. `viewed_seconds` is the average in-view duration per measurable impression — reporting-side counterpart to the `viewed_seconds` optimization goal, governed by the same `standard` threshold as `viewable_rate`. The optional `vendor` field carries a `BrandRef` so the row is self-describing — buyer agents reading delivery in isolation can attribute the numbers to a measurement vendor without joining back to `package.committed_metrics` or `package.performance_standards`.
- **`quartile_data`**: Video quartile completion data (q1-q4); `null` when not applicable (e.g. a non-video buy)
- **`time_based_views`**: Time-threshold view counts, one entry per (threshold_seconds, basis). `play_time` (platform-counted playback time, no independent viewability qualification) and `in_view` (IAB/MRC viewable-video clock, 50% of pixels) are not comparable and must not be summed. 2s and 6s are RECOMMENDED cross-platform reporting points; sellers may report other thresholds. Primarily an autoplay/skippable-video metric (social, olv, in-feed video) — completion (`quartile_data`) remains the currency for lean-back CTV/cinema inventory. Disclosure-grade: a `committed_metrics` entry for `time_based_views` contracts the array's presence, not specific thresholds.
- **`dooh_metrics`**: DOOH-specific metrics (loop plays, screens, venue breakdown)
- **`cost_per_click`**: Cost per click (`spend / clicks`)
- **`cost_per_completed_view`**: Cost per completed view (`spend / completed_views`); CPCV pricing scalar for video/audio inventory
Expand All @@ -322,6 +323,8 @@ Buyers receive the intersection of both. `impressions` and `spend` are always re
- **`new_to_brand_units`**: Unit-volume parallel to `new_to_brand_rate` — count of units sold to first-time brand buyers
- **`plays`**: Raw play count for DOOH/broadcast inventory (mirrors `forecastable-metric.plays`); distinct from `dooh_metrics.loop_plays` (per-screen rotation) and `impressions` (multiplied audience figure)

The numeric values inside `viewability` and `quartile_data` are individually addressable through leaf metric identities — `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds`, and `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` (which resolve to `quartile_data.q1_views`–`q4_views`). Leaf identities appear in `available_metrics` declarations, `committed_metrics` contracts, `metric_aggregates` rows, and breakdown `sort_by`; the nested object remains the only carrier of the values in delivery reports. In every metric set operation (declaration, filtering, intersection, selection) a container token subsumes its leaf identities — declaring `viewability` satisfies `viewable_rate`. Note that `quartile_100` counts 100%-of-duration completions (VAST `complete`), which is distinct from `completed_views` when the buy defines a shorter billable view threshold.

Buyers can optionally request a subset via `requested_metrics` to reduce payload size and focus on relevant KPIs.

For `completion_rate` and `quartile_data`, sellers MAY return `null` to signal the metric does not apply (e.g. on a non-video buy), and clients MUST accept `null` as a valid value for these two fields. Every other metric signals "not applicable" by omission — sellers omit it rather than sending `null`.
Expand Down
3 changes: 2 additions & 1 deletion docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ asyncio.run(main())
| **Reach Window** | Window semantics for reported reach/frequency: `cumulative` (uniques since campaign start), `period` (uniques within a single non-overlapping reporting period — e.g., daily snapshot), or `rolling` (uniques within a trailing window — e.g., trailing-7-day). Never sum across rows. Optional but strongly recommended when reach is present. |
| **Frequency** | Average ad exposures per reach unit, measured over `reach_window` |
| **Viewability** | Object with `vendor`, `measurable_impressions` (denominator), `viewable_impressions`, `viewable_rate`, `viewed_seconds` (average in-view duration per measurable impression — pairs with the `viewed_seconds` optimization goal), and `standard` |
| **Time-based views** | Array of `{threshold_seconds, basis, views}`. One entry per (threshold_seconds, basis) pair. `basis` is `play_time` (continuous playback time, e.g. platform 2s/6s video views) or `in_view` (continuous in-view time under the governing viewability standard); the two are not comparable and must not be summed |
| **Follows** | New followers, page likes, or free channel/feed subscribes attributed to delivery |
| **Pacing Index** | Actual vs. expected delivery rate (1.0 = on track, <1.0 = behind, >1.0 = ahead) |
| **CPM** | Cost per thousand impressions (spend/impressions * 1000) |
Expand Down Expand Up @@ -895,7 +896,7 @@ When you include `reporting_dimensions` in the request, the response includes di
}
```

Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported.
Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). The sort vocabulary includes leaf metric identities that order rows by a value nested inside an object-shaped metric — `sort_by: "quartile_100"` orders by `quartile_data.q4_views`, `sort_by: "viewable_rate"` orders by `viewability.viewable_rate` — so queries like "top placements by in-view rate" need no flat duplicate fields. Survey/model-based lift scalars are excluded from sorting; see the `sort-metric` enum description. Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported.

### Available dimensions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"deploy:cdn-artifacts-cutover:dry-run": "wrangler deploy --config workers/artifact-cdn/wrangler.cutover.toml --dry-run",
"verify:cdn-artifacts-cutover": "node scripts/verify-cdn-artifacts-cutover.mjs",
"typecheck": "tsc --project server/tsconfig.json --noEmit",
"test:schemas": "node tests/schema-validation.test.cjs && node --test tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/schema-deprecation-metadata.test.cjs tests/creative-rotation.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs && npm run test:geo-region-targeting",
"test:schemas": "node tests/schema-validation.test.cjs && node --test tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/schema-deprecation-metadata.test.cjs tests/creative-rotation.test.cjs tests/metric-identity-coherence.test.cjs tests/time-based-views-contract.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs && npm run test:geo-region-targeting",
"test:performance-feedback": "node --test --test-force-exit --test-timeout=30000 tests/performance-feedback-contract.test.cjs",
"test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs",
"test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge",
Expand Down
33 changes: 31 additions & 2 deletions static/schemas/source/core/delivery-metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
},
"quartile_data": {
"type": ["object", "null"],
"description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).",
"description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy). Individual quartiles are addressable via the leaf metric identities `quartile_25` (q1_views), `quartile_50` (q2_views), `quartile_75` (q3_views), and `quartile_100` (q4_views) for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. Quartiles are player-fired events (VAST firstQuartile/midpoint/thirdQuartile/complete). `quartile_100` counts 100%-of-duration completions and is distinct from `completed_views`, which counts completions at the seller's billable view threshold (`view_duration_seconds`) when one is set.",
"properties": {
"q1_views": {
"type": "number",
Expand All @@ -213,6 +213,35 @@
}
}
},
"time_based_views": {
"type": "array",
"description": "Time-threshold video view counts. Each entry reports views that met a continuous duration threshold under a stated basis, rather than a completion percentage (percentage-based completion is quartile_data). Thresholds of 2 and 6 seconds are RECOMMENDED cross-platform reporting points; any seller-defined threshold is permitted. One entry per (threshold_seconds, basis) pair per reporting period — sellers MUST de-duplicate before emission and MUST NOT emit the same pair twice; buyers MAY treat duplicate pairs as a seller-side conformance bug. Entries under different bases are different metrics and MUST NOT be summed (see view-threshold-basis). Primarily an autoplay/skippable-video metric (social, olv, in-feed video); completion metrics remain the currency for lean-back CTV/cinema inventory. Distinct from `views` (the single billable-threshold scalar) and from `viewability.viewed_seconds` (average in-view duration, not a threshold count). Array entries are not individually sortable in breakdown sort_by. Disclosure-grade surface: (threshold_seconds, basis) is not part of the committed-metric qualifier vocabulary, so a `committed_metrics` entry for `time_based_views` contracts the array's presence, not specific thresholds.",
"items": {
"type": "object",
"properties": {
"threshold_seconds": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Continuous duration threshold in seconds an impression must meet to count as a view in this entry."
},
"basis": {
"$ref": "/schemas/enums/view-threshold-basis.json",
"description": "Whether the threshold clock runs on playback time or in-view time. Required because play-time and in-view counts at the same threshold are materially different numbers."
},
"views": {
"type": "number",
"minimum": 0,
"description": "Count of views meeting this entry's threshold and basis."
},
"standard": {
"$ref": "/schemas/enums/viewability-standard.json",
"description": "Viewability standard governing the in-view clock for this entry. RECOMMENDED when basis is 'in_view' (MRC and GroupM thresholds differ); not applicable to play_time entries."
}
},
"required": ["threshold_seconds", "basis", "views"],
"additionalProperties": true
}
},
"dooh_metrics": {
"type": "object",
"description": "DOOH-specific metrics (only included for DOOH campaigns)",
Expand Down Expand Up @@ -288,7 +317,7 @@
},
"viewability": {
"type": "object",
"description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable.",
"description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. The numeric leaves are addressable via the leaf metric identities `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. When a buy reports under more than one standard, contract a specific standard via the `viewability_standard` qualifier on `committed_metrics`; when the package's `committed_metrics` carry a `viewability_standard` qualifier, sellers MUST populate `standard` on reported viewability objects so reconciliation can match the qualifier.",
"properties": {
"vendor": {
"$ref": "/schemas/core/brand-ref.json",
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/format.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@
},
"reported_metrics": {
"type": "array",
"description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. If omitted, the format defers entirely to product-level metric declarations.",
"description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. The intersection is evaluated under the container-subsumption rule in `enums/available-metric.json` — a container token (e.g. `viewability`) matches its leaf identities (e.g. `viewable_rate`), so mixed container/leaf declarations do not produce an empty intersection. If omitted, the format defers entirely to product-level metric declarations.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/product-filters.json
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@
},
"required_metrics": {
"type": "array",
"description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.",
"description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). Superset evaluation follows the container-subsumption rule in `enums/available-metric.json` — a product declaring the `viewability` container satisfies a requirement for the `viewable_rate` leaf. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/reporting-webhook.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
},
"requested_metrics": {
"type": "array",
"description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics.",
"description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics. Subset evaluation and leaf resolution follow the container-subsumption rule in `enums/available-metric.json`: requesting a leaf identity selects its canonical carrier object in the payload.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
Loading
Loading