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/negotiated-automatic-breakdowns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Make the automatic delivery breakdowns (creative, keyword, catalog_item) optionally negotiable: including their keys in `reporting_dimensions` adds `limit`/`sort_by`/`sort_direction` control and makes the new `by_X_truncated` and applied-sort echo fields binding, so "top creatives by quartile_100" is answerable with a completeness contract. Omitting the keys preserves today's automatic behavior exactly. Implements RFC #6623.
15 changes: 12 additions & 3 deletions docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Retrieve comprehensive delivery metrics and performance data for media buy repor
| `status_filter` | string \| string[] | No | Status filter: `"pending_creatives"`, `"pending_start"`, `"active"`, `"paused"`, `"completed"`. Defaults to `["active"]` when omitted. |
| `start_date` | string | No | Report start date (YYYY-MM-DD), inclusive. Omit for campaign lifetime data. Only accepted when product supports `date_range`. |
| `end_date` | string | No | Report end date (YYYY-MM-DD), **exclusive**. Omit for campaign lifetime data. Only accepted when product supports `date_range`. |
| `reporting_dimensions` | object | No | Request dimensional breakdowns within `by_package`. Include a key as an empty object (e.g., `"device_type": {}`) to activate with defaults. Keys: `geo`, `device_type`, `device_platform`, `audience`, `demographic`, `placement`, `spot`. Most accept optional `limit`, `sort_by`, and `sort_direction`. Demographic also accepts canonical `age_ranges`, each of which must match the product's independent demographic reporting capability exactly. `spot` accepts only `limit`, returns rows by `aired_at` ascending, and defaults to the complete log for the reporting period. Geo requires `geo_level`; include `system` for metro/postal levels. Unsupported dimensions are silently omitted; unsupported requested demographic ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); malformed requests return a validation error. |
| `reporting_dimensions` | object | No | Request dimensional breakdowns within `by_package`. Include a key as an empty object (e.g., `"device_type": {}`) to activate with defaults. Keys: `geo`, `device_type`, `device_platform`, `audience`, `demographic`, `placement`, `spot`, `creative`, `keyword`, `catalog_item`. Most accept optional `limit`, `sort_by`, and `sort_direction`. Demographic also accepts canonical `age_ranges`, each of which must match the product's independent demographic reporting capability exactly. `spot` accepts only `limit`, returns rows by `aired_at` ascending, and defaults to the complete log for the reporting period. Geo requires `geo_level`; include `system` for metro/postal levels. `creative`, `keyword`, and `catalog_item` are returned automatically when the seller supports them, even when omitted from this object — including one of their keys is optional and upgrades that breakdown to a negotiated contract (`limit`, `sort_by`, `sort_direction`, and a binding truncation/sort-echo disclosure) without changing the automatic default. Unsupported dimensions are silently omitted; unsupported requested demographic ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); malformed requests return a validation error. |
| `time_granularity` | string | No | Per-window slice granularity for pull recovery, matching `reporting_webhook.reporting_frequency` vocabulary (`hourly`, `daily`, `weekly`, `monthly`, `quarterly`, `post_campaign`). When set, the response includes `windows[]` slices shape-aligned with webhook fires at the same granularity. Capability-scoped — value MUST be in the product's `reporting_capabilities.windowed_pull_granularities`. See [Windowed pull recovery](#windowed-pull-recovery). |
| `include_window_breakdown` | boolean | No | When `true` (and `time_granularity` is set), include the `windows[]` array on each media buy. Defaults to `false`. Ignored when `time_granularity` is omitted. |
| `requested_metrics` | string[] | No | Narrow the response to a subset of metrics — same semantics as `reporting_webhook.requested_metrics`. Applies to `totals`, `by_package`, daily/window slices, and breakdown rows. When omitted, all available metrics are included (unchanged behavior). See [Requesting a metric subset](#requesting-a-metric-subset). |
Expand Down Expand Up @@ -752,6 +752,8 @@ Each creative entry includes:

For deeper creative analytics including variant-level delivery data (asset combination optimization, generative creative), use [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery). This is a Creative Protocol task — call it on any agent that implements the Creative Protocol, which may be the same sales agent if it declares `"creative"` in `supported_protocols`. See [Creative capabilities on sales agents](/docs/creative/sales-agent-creative-capabilities).

By default `by_creative` is returned automatically at the seller's discretion, with no limit, sort, or truncation contract. To answer questions like "top creatives by completion rate," include `reporting_dimensions: { "creative": { "limit": 10, "sort_by": "quartile_100" } }` in the request — this negotiates the breakdown, making `by_creative_truncated`, `by_creative_sorted_by`, and `by_creative_sort_direction` binding. See [Dimension Breakdowns](#dimension-breakdowns).

## Catalog-item reporting

For catalog-driven packages (packages with a `catalog` field), the seller can return per-catalog-item delivery in the `by_catalog_item` array within each package.
Expand All @@ -766,6 +768,8 @@ Each entry identifies the catalog item and includes standard delivery metrics:

This is optional. Sellers that support item-level reporting populate `by_catalog_item`; sellers that do not simply omit it.

By default `by_catalog_item` is returned automatically at the seller's discretion, with no limit, sort, or truncation contract. Including `reporting_dimensions.catalog_item` (e.g., `{ "limit": 25, "sort_by": "roas" }`) negotiates the breakdown, making `by_catalog_item_truncated`, `by_catalog_item_sorted_by`, and `by_catalog_item_sort_direction` binding. See [Dimension Breakdowns](#dimension-breakdowns).

```json
{
"by_package": [
Expand Down Expand Up @@ -900,7 +904,9 @@ When you include `reporting_dimensions` in the request, the response includes di
}
```

Geo, device, audience, demographic, and placement dimensions accept optional `limit`, `sort_by` (any value from the `sort-metric` enum; defaults to `spend`), and `sort_direction` (`asc` or `desc`; defaults to `desc`, largest first). 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. Setting `sort_direction: "asc"` enables bottom-N queries — for example, the 25 worst placements by `viewable_rate` — that cannot be recovered from a truncated descending pull. The seller echoes the sort it actually applied on each breakdown: `by_geo_sorted_by`/`by_geo_sort_direction`, `by_device_type_sorted_by`/`by_device_type_sort_direction`, and the equivalent pair for `device_platform`, `audience`, `demographic`, and `placement`. Sellers MUST return both echo fields whenever the corresponding breakdown array is present, including when they fall back to sorting by `spend` because they don't report the requested metric — the echo makes that fallback visible instead of leaving the buyer to misread the rows as ordered by the requested metric. 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, placement, creative, keyword, and catalog_item dimensions accept optional `limit`, `sort_by` (any value from the `sort-metric` enum; defaults to `spend`), and `sort_direction` (`asc` or `desc`; defaults to `desc`, largest first). 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 creatives by completion" need no flat duplicate fields. Survey/model-based lift scalars are excluded from sorting; see the `sort-metric` enum description. Setting `sort_direction: "asc"` enables bottom-N queries — for example, the 25 worst placements by `viewable_rate` — that cannot be recovered from a truncated descending pull. The seller echoes the sort it actually applied on each breakdown: `by_geo_sorted_by`/`by_geo_sort_direction`, `by_device_type_sorted_by`/`by_device_type_sort_direction`, and the equivalent pair for `device_platform`, `audience`, `demographic`, `placement`, `creative`, `keyword`, and `catalog_item`. For every dimension requested via `reporting_dimensions`, sellers MUST return both echo fields whenever the corresponding breakdown array is present, including when they fall back to sorting by `spend` because they don't report the requested metric — the echo makes that fallback visible instead of leaving the buyer to misread the rows as ordered by the requested metric. (When `creative`, `keyword`, or `catalog_item` rows are returned automatically without a request key, the echo and truncation fields are RECOMMENDED, not required — see the automatic-breakdown contract below.) 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.

`creative`, `keyword`, and `catalog_item` are different from the other dimensions in one respect: sellers return them automatically whenever they support the underlying capability, whether or not the buyer's `reporting_dimensions` mentions them. Omitting the key preserves that automatic default — rows show up with no completeness contract, and `by_creative_truncated`/`by_keyword_truncated`/`by_catalog_item_truncated` and their sort echoes are only RECOMMENDED. Including the key (even as `{}`) negotiates the breakdown: `limit` and `sort_by`/`sort_direction` are honored, and the truncation flag and applied-sort echo become MUST-return fields, the same as any other requested dimension.

### Available dimensions

Expand All @@ -913,6 +919,9 @@ Geo, device, audience, demographic, and placement dimensions accept optional `li
| Demographic | `by_demographic` | `demographic`, `demographic_system`, `impressions`, `spend` | `age`, other delivery metrics | `supports_demographic_breakdown` |
| Placement | `by_placement` | `placement_id`, `impressions`, `spend` | `publisher_domain`, `placement_name` | `supports_placement_breakdown` |
| Spot/as-run | `by_spot` | `spot_id`, `aired_at` | `impressions`, `network`, `station`, `daypart`, other delivery metrics | `supports_spot_breakdown` |
| Creative | `by_creative` | `creative_id`, `impressions`, `spend` | `weight`, other delivery metrics | `supports_creative_breakdown` (automatic; `reporting_dimensions.creative` optionally negotiates limit/sort) |
| Keyword | `by_keyword` | `keyword`, `match_type`, `impressions`, `spend` | other delivery metrics | Search/retail media capability (automatic; `reporting_dimensions.keyword` optionally negotiates limit/sort) |
| Catalog item | `by_catalog_item` | `content_id`, `content_id_type`, `impressions`, `spend` | other delivery metrics | Catalog-driven packages only (automatic; `reporting_dimensions.catalog_item` optionally negotiates limit/sort) |

Check `reporting_capabilities` on the product to discover which dimensions are available. Product-level capabilities are authoritative since different products from the same seller may support different breakdowns.

Expand All @@ -928,7 +937,7 @@ Delivery metrics on a spot row are optional. Omission means the metric is not ye

### Truncation

Each breakdown array has a sibling boolean flag (e.g., `by_geo_truncated`). When `true`, additional rows exist beyond the returned set. When `false`, the list is complete. Sellers MUST return the truncated flag whenever the corresponding breakdown array is present. Metric dimensions are sorted by the requested `sort_by` value in the requested `sort_direction` (descending by default); the seller echoes the metric and direction it actually applied in `by_*_sorted_by`/`by_*_sort_direction`, which sellers MUST return whenever the breakdown array is present. Spot rows always use `aired_at` ascending and carry neither field.
Each breakdown array has a sibling boolean flag (e.g., `by_geo_truncated`). When `true`, additional rows exist beyond the returned set. When `false`, the list is complete. For every dimension requested via `reporting_dimensions`, sellers MUST return the truncated flag whenever the corresponding breakdown array is present. Metric dimensions are sorted by the requested `sort_by` value in the requested `sort_direction` (descending by default); the seller echoes the metric and direction it actually applied in `by_*_sorted_by`/`by_*_sort_direction`, which sellers MUST return whenever a requested dimension's breakdown array is present. For `by_creative`, `by_keyword`, and `by_catalog_item` rows returned automatically without a request key, the truncated and echo fields are RECOMMENDED rather than required — automatic rows carry no completeness contract. Spot rows always use `aired_at` ascending and carry neither field.

Demographic reporting also returns `by_demographic_suppressed`. `true` means privacy, policy, or measurement thresholds omitted otherwise reportable rows, so the visible rows may not reconcile to package totals. This is distinct from truncation caused by a row limit.

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/metric-identity-coherence.test.cjs tests/sort-contract-delivery-reporting.test.cjs tests/requested-metrics-contract.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/sort-contract-delivery-reporting.test.cjs tests/requested-metrics-contract.test.cjs tests/auto-breakdown-negotiation-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
Loading
Loading