Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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 semantics as reporting_webhook.requested_metrics.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: "Same semantics as reporting_webhook.requested_metrics" isn't literally true, in two ways. (1) This field adds minItems: 1; the webhook field (core/reporting-webhook.json:53-59) has no minItems, so [] is valid on the webhook and rejected here — a client sharing metric-narrowing validation across both paths hits the divergence. (2) The "impressions and spend are always included" rule is asserted here but absent from the webhook description; if the webhook shares that behavior, it belongs on the webhook side too (single source of truth), otherwise the two contracts genuinely differ. The parity guard in tests/requested-metrics-contract.test.cjs only checks type and items.$ref, so it won't catch either drift. Either align the constraints or soften the "identical semantics" wording in both schemas and the two docs pages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest commit: the always-included impressions/spend rule now lives on the webhook field too, the webhook's empty-array case is defined as equivalent to omission, and both descriptions state the one intentional shape difference (GET requires ≥1 entry when present; omit for full payloads) instead of claiming identical semantics.

"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