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/get-delivery-requested-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add `requested_metrics` to `get_media_buy_delivery`, giving the GET path the same metric narrowing the reporting webhook already has. Omitted means unchanged full payloads; impressions and spend are always included; requesting a leaf metric identity returns its canonical nested carrier; and `missing_metrics` MUST NOT flag absences caused solely by request narrowing. Implements RFC #6624.
2 changes: 1 addition & 1 deletion docs/media-buy/media-buys/optimization-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ Buyers receive the intersection of both. `impressions` and `spend` are always re

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.
Buyers can optionally request a subset via `requested_metrics` to reduce payload size and focus on relevant KPIs. The same field exists on the [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery#requesting-a-metric-subset) request with identical semantics, giving the GET path the same narrowing the webhook already has.

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
5 changes: 5 additions & 0 deletions docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Retrieve comprehensive delivery metrics and performance data for media buy repor
| `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. |
| `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). |

> **Date Range Behavior**: The date range is **start-inclusive, end-exclusive**. For example, `start_date: "2026-01-01"` and `end_date: "2026-01-02"` returns delivery data for January 1st only (from `2026-01-01 00:00:00` up to, but not including, `2026-01-02 00:00:00`). To get a full week of data (Jan 1-7), use `end_date: "2026-01-08"`.

Expand Down Expand Up @@ -650,6 +651,10 @@ asyncio.run(main())
- **Attribution window**: `attribution_window` describes the lookback windows and model used for conversion attribution (e.g., 14-day click, 1-day view, last_touch)
- **Package-level**: All metrics broken down by package with pacing_index

### Requesting a metric subset

`requested_metrics` narrows the response to a specific list of metrics — the same field and semantics as `reporting_webhook.requested_metrics`, now also available on the GET path. It applies to every metrics-bearing object in the response: `totals`, `by_package`, daily and window slices, and breakdown rows. `impressions` and `spend` are always included regardless of what the list contains. Requesting a leaf metric identity (e.g. `viewable_rate`, `quartile_75`) returns its canonical nested carrier (`viewability`, `quartile_data`) rather than a flat duplicate field. A metric requested but not available for the buy is simply omitted — no error — and `missing_metrics` is unaffected by that omission: it still reconciles solely against `committed_metrics`, and sellers MUST NOT list a metric there just because the buyer excluded it via `requested_metrics`. Values outside the product's `reporting_capabilities.available_metrics` are ignored. Omitting the field preserves current behavior — all available metrics are returned.

## Data Freshness

- Reporting data typically has 2-4 hour delay
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/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/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
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. 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.",
"description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included; an empty array has the same meaning as omission (it does not narrow to impressions and spend only). impressions and spend are always included regardless of this list. 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. Same narrowing semantics as get_media_buy_delivery's requested_metrics (which additionally requires at least one entry when present).",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
"default": false,
"description": "When true, include daily_breakdown arrays within each package in by_package. Useful for per-package pacing analysis and line-item monitoring. Omit or set false to reduce response size — package daily data can be large for multi-package buys over long flights."
},
"requested_metrics": {
"type": "array",
"description": "Optional list of metrics to include in the response. When omitted, all available metrics are included (unchanged behavior). Applies to every metrics-bearing object in the response: totals, by_package, daily and window slices, and breakdown rows. impressions and spend are always included regardless of this list. Requesting a leaf metric identity returns its canonical nested carrier — e.g. requesting viewable_rate returns the viewability object, requesting quartile_75 returns quartile_data — never a flat duplicate. Metrics requested but not available for this buy are omitted from the response without error; contract accountability is unchanged — missing_metrics still reconciles against committed_metrics, but sellers MUST NOT list a metric in missing_metrics when its absence is solely due to this narrowing. Must be a subset of the product's reporting_capabilities.available_metrics; values outside the declared set are ignored. Subset evaluation follows the container-subsumption rule in enums/available-metric.json. Sort is evaluated before narrowing: excluding a metric from this list never triggers the sort_by fallback, and breakdown rows may be ordered by a metric absent from the narrowed payload — the applied-sort echo still names it. Same narrowing semantics as reporting_webhook.requested_metrics, with one shape difference: this field requires at least one entry when present (omit it entirely for full payloads), while the webhook field permits an empty array with the same meaning as omission.",
"items": { "$ref": "/schemas/enums/available-metric.json" },
"uniqueItems": true,
"minItems": 1
},
"time_granularity": {
"$ref": "/schemas/enums/reporting-frequency.json",
"description": "Per-window slice granularity for the pull, using the same vocabulary as reporting_webhook.reporting_frequency. When set, the seller returns per-window delivery slices over the date range — useful for reconstructing data a buyer's webhook receiver missed, since the slice payload is shape-aligned with what reporting_webhook would have delivered for the same window. Capability-scoped: the value MUST be one of the seller's declared reporting_capabilities.windowed_pull_granularities; otherwise the seller MUST return UNSUPPORTED_GRANULARITY. When omitted, behavior is unchanged (cumulative aggregates plus optional daily breakdowns per existing fields)."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@
},
"missing_metrics": {
"type": "array",
"description": "Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when `package.committed_metrics` is present, `missing_metrics` is computed against entries where `committed_at < reporting_period.end` — independent of subsequent product mutations and respecting the commitment timestamp on each entry (a metric committed mid-flight is only flagged missing in reports for periods after its commitment). When `package.committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics` (no timestamp filter). Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. Each entry uses an explicit `scope` discriminator: `standard` for entries from the closed `available-metric.json` enum, `vendor` for vendor-defined metrics anchored on a BrandRef. Symmetric with `committed_metrics`.",
"description": "Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when `package.committed_metrics` is present, `missing_metrics` is computed against entries where `committed_at < reporting_period.end` — independent of subsequent product mutations and respecting the commitment timestamp on each entry (a metric committed mid-flight is only flagged missing in reports for periods after its commitment). When `package.committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics` (no timestamp filter). Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. Each entry uses an explicit `scope` discriminator: `standard` for entries from the closed `available-metric.json` enum, `vendor` for vendor-defined metrics anchored on a BrandRef. Symmetric with `committed_metrics`. When the request narrowed the payload via requested_metrics, sellers MUST NOT list a committed metric here solely because the buyer excluded it — missing_metrics reports delivery gaps, not request narrowing.",
"items": {
"$ref": "/schemas/core/missing-metric.json"
},
Expand Down
108 changes: 108 additions & 0 deletions tests/requested-metrics-contract.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const fs = require("fs");
const path = require("path");
const Ajv = require("ajv");
const addFormats = require("ajv-formats");
const { describe, it, before } = require("node:test");
const assert = require("node:assert/strict");

const SCHEMA_ROOT = path.join(__dirname, "..", "static", "schemas", "source");

function readSchema(uri) {
assert.match(uri, /^\/schemas\//);
return JSON.parse(
fs.readFileSync(path.join(SCHEMA_ROOT, uri.slice("/schemas/".length)), "utf8")
);
}

async function compile(schema) {
const ajv = new Ajv({
allErrors: true,
strict: false,
loadSchema: async (ref) => readSchema(ref),
});
addFormats(ajv);
return ajv.compileAsync(schema);
}

describe("requested_metrics contract (get_media_buy_delivery)", () => {
let validateRequest;
let requestJson;
let responseJson;
let webhookJson;

before(async () => {
requestJson = readSchema(
"/schemas/media-buy/get-media-buy-delivery-request.json"
);
responseJson = readSchema(
"/schemas/media-buy/get-media-buy-delivery-response.json"
);
webhookJson = readSchema("/schemas/core/reporting-webhook.json");

validateRequest = await compile(requestJson);
});

it("accepts a valid requested_metrics list of standard metrics", () => {
assert.equal(
validateRequest({
requested_metrics: ["impressions", "spend", "quartile_100"],
}),
true,
JSON.stringify(validateRequest.errors)
);
});

it("accepts a requested_metrics list containing a leaf metric identity", () => {
assert.equal(
validateRequest({ requested_metrics: ["viewable_rate"] }),
true,
JSON.stringify(validateRequest.errors)
);
});

it("rejects an empty requested_metrics array (minItems)", () => {
assert.equal(validateRequest({ requested_metrics: [] }), false);
});

it("rejects a requested_metrics value outside the available-metric enum", () => {
assert.equal(
validateRequest({ requested_metrics: ["bogus_metric"] }),
false
);
});

it("rejects duplicate entries in requested_metrics (uniqueItems)", () => {
assert.equal(
validateRequest({ requested_metrics: ["clicks", "clicks"] }),
false
);
});

it("request field description states impressions and spend are always included", () => {
assert.match(
requestJson.properties.requested_metrics.description,
/impressions and spend are always included/
);
});

it("response missing_metrics description states requested_metrics narrowing MUST NOT be flagged", () => {
const missingMetricsDescription =
responseJson.properties.media_buy_deliveries.items.properties.by_package
.items.allOf[1].properties.missing_metrics.description;

assert.match(
missingMetricsDescription,
/MUST NOT list a committed metric here solely because the buyer excluded it/
);
});

it("reporting_webhook still declares requested_metrics with available-metric items (parity guard)", () => {
const webhookField = webhookJson.properties.requested_metrics;
assert.ok(webhookField, "reporting-webhook.json is missing requested_metrics");
assert.equal(webhookField.type, "array");
assert.equal(
webhookField.items.$ref,
"/schemas/enums/available-metric.json"
);
});
});
Loading