Problem
The snapshot/log foundation contract (#4582, snapshot-and-log.mdx) commits AdCP to a two-path principle:
Either path is complete. A buyer using webhooks reliably gets all the data. A buyer using only GET (no webhooks) gets the same data. The buyer chooses based on latency, ergonomics, and receiver infrastructure.
For this to hold, every webhook event MUST correspond to data retrievable via GET, at the same granularity the webhook delivers — for the granularities the seller declares supported.
Today's get_media_buy_delivery accepts start_date / end_date for arbitrary windows (when reporting_capabilities.date_range_support: date_range), and include_package_daily_breakdown: true for daily breakdowns. But reporting_webhook supports reporting_frequency: hourly | daily | monthly. There is no way for a buyer to pull the hourly windows that a webhook would have delivered.
Concrete failure case:
- Buyer configures
reporting_webhook with reporting_frequency: hourly.
- Buyer's webhook receiver is offline for 6 hours.
- Six hourly fires fail; transport retries eventually expire.
- Buyer recovers their receiver, calls
get_media_buy_delivery with the relevant date range.
- Buyer gets daily aggregates and the cumulative-to-date totals. Per-hour detail is lost. Reconciliation against the buyer's internal hourly model is impossible.
This is exactly the "data-bearing event" weakness called out in snapshot-and-log.mdx Rule 4.
Proposal
Extend get_media_buy_delivery to expose windowed pulls. Capability-scoped: sellers declare which granularities they support pulling, and MUST honor pulls at any declared granularity. Industry naming (time_granularity) matches DV360 / TTD / Xandr reporting APIs.
Request additions
{
"media_buy_ids": ["mb_..."],
"start_date": "2026-06-01",
"end_date": "2026-06-02",
"time_granularity": "hourly", // NEW — matches DV360 groupBy:HOUR, TTD TimeBreakdown
"include_window_breakdown": true // NEW
}
time_granularity — one of hourly, daily, monthly (same vocabulary as reporting_webhook.reporting_frequency). When set, the seller returns per-window slices over the date range. When omitted, behavior is unchanged (cumulative aggregates per current spec).
include_window_breakdown — when true, the response includes a windows[] array of per-window delivery slices, each shaped to match what reporting_webhook would have delivered for that window. Sellers MUST return error UNSUPPORTED_GRANULARITY when time_granularity is not in their declared windowed_pull_granularities.
Response addition
{
"media_buys": [
{
"media_buy_id": "mb_...",
"lifetime_delivery": { /* existing */ },
"windows": [
{
"window_start": "2026-06-01T00:00:00Z",
"window_end": "2026-06-01T01:00:00Z",
"metrics": { "impressions": 12345, "spend": 67.89, /* ... */ },
"by_package": [ /* if include_package_daily_breakdown semantics extended to windows */ ]
}
]
}
]
}
The windows[] entries SHOULD match the payload shape of reporting_webhook fires for the same granularity. A buyer who missed a webhook can reconstruct identical data by pulling the corresponding window.
Capability declaration
Add to reporting-capabilities.json:
{
"windowed_pull_granularities": ["daily"], // honestly declared
// existing: "available_metrics", "date_range_support", "measurement_windows", ...
}
Sellers declare which granularities they support pulling. Capability-scoped MUST: a seller MUST honor pull requests at any granularity in this set. A seller MAY emit higher-frequency webhooks than they expose for pull — common in stream-tap architectures where the webhook is a Kafka tap and historical pulls go through a warehouse with coarser granularity. In that case, the buyer knows up front (via the capability) that hourly pull-recovery is unavailable from that seller and treats the webhook as primary for that frequency.
The two-paths principle still holds for the declared parity set. Sellers that want to claim universal two-paths-parity declare windowed_pull_granularities matching every reporting_frequency they offer. Sellers with backend constraints declare honestly.
This avoids the failure mode where a strict MUST forces mid-size SSPs (whose webhook pipeline is a Kafka tap, not a warehouse query) to either rebuild their backend or downgrade their webhook to daily to dodge the requirement. Honest declaration is the right outcome.
Backwards compatibility
- New request fields are optional. Old callers omit them; behavior unchanged.
- New response field
windows[] is optional; absent unless requested.
- New capability field documents support; sellers that don't add it are treated as
windowed_pull_granularities: [] — they can only do lifetime/date-range cumulative pulls (matches current behavior).
No breaking changes; safe in 3.1 as additive.
Why this matters
Without this, the snapshot/log contract overpromises on data-bearing channels. With this, every channel has either full parity or honest declared limits — buyers know what they can recover from polling vs. what requires reliable webhook delivery.
Combined with #4278 (webhook_activity[]) which closes the per-fire transport log, the snapshot/log contract becomes: two complete paths within each seller's declared capability, buyer's choice.
Out of scope
- Real-time streaming reporting (sub-hourly windows). If sellers want this, they can declare
["hourly", "daily", "monthly", "minutely"] or whatever — but that's a seller-capability question, not a spec gap.
- Cross-window aggregation logic (buyers compute their own rollups from window slices).
- Other data-bearing webhook channels beyond
reporting_webhook (e.g., conversion event firehose if/when added).
Related
Acceptance
Problem
The snapshot/log foundation contract (#4582,
snapshot-and-log.mdx) commits AdCP to a two-path principle:For this to hold, every webhook event MUST correspond to data retrievable via GET, at the same granularity the webhook delivers — for the granularities the seller declares supported.
Today's
get_media_buy_deliveryacceptsstart_date/end_datefor arbitrary windows (whenreporting_capabilities.date_range_support: date_range), andinclude_package_daily_breakdown: truefor daily breakdowns. Butreporting_webhooksupportsreporting_frequency: hourly | daily | monthly. There is no way for a buyer to pull the hourly windows that a webhook would have delivered.Concrete failure case:
reporting_webhookwithreporting_frequency: hourly.get_media_buy_deliverywith the relevant date range.This is exactly the "data-bearing event" weakness called out in
snapshot-and-log.mdxRule 4.Proposal
Extend
get_media_buy_deliveryto expose windowed pulls. Capability-scoped: sellers declare which granularities they support pulling, and MUST honor pulls at any declared granularity. Industry naming (time_granularity) matches DV360 / TTD / Xandr reporting APIs.Request additions
{ "media_buy_ids": ["mb_..."], "start_date": "2026-06-01", "end_date": "2026-06-02", "time_granularity": "hourly", // NEW — matches DV360 groupBy:HOUR, TTD TimeBreakdown "include_window_breakdown": true // NEW }time_granularity— one ofhourly,daily,monthly(same vocabulary asreporting_webhook.reporting_frequency). When set, the seller returns per-window slices over the date range. When omitted, behavior is unchanged (cumulative aggregates per current spec).include_window_breakdown— when true, the response includes awindows[]array of per-window delivery slices, each shaped to match whatreporting_webhookwould have delivered for that window. Sellers MUST return errorUNSUPPORTED_GRANULARITYwhentime_granularityis not in their declaredwindowed_pull_granularities.Response addition
{ "media_buys": [ { "media_buy_id": "mb_...", "lifetime_delivery": { /* existing */ }, "windows": [ { "window_start": "2026-06-01T00:00:00Z", "window_end": "2026-06-01T01:00:00Z", "metrics": { "impressions": 12345, "spend": 67.89, /* ... */ }, "by_package": [ /* if include_package_daily_breakdown semantics extended to windows */ ] } ] } ] }The
windows[]entries SHOULD match the payload shape ofreporting_webhookfires for the same granularity. A buyer who missed a webhook can reconstruct identical data by pulling the corresponding window.Capability declaration
Add to
reporting-capabilities.json:{ "windowed_pull_granularities": ["daily"], // honestly declared // existing: "available_metrics", "date_range_support", "measurement_windows", ... }Sellers declare which granularities they support pulling. Capability-scoped MUST: a seller MUST honor pull requests at any granularity in this set. A seller MAY emit higher-frequency webhooks than they expose for pull — common in stream-tap architectures where the webhook is a Kafka tap and historical pulls go through a warehouse with coarser granularity. In that case, the buyer knows up front (via the capability) that hourly pull-recovery is unavailable from that seller and treats the webhook as primary for that frequency.
The two-paths principle still holds for the declared parity set. Sellers that want to claim universal two-paths-parity declare
windowed_pull_granularitiesmatching everyreporting_frequencythey offer. Sellers with backend constraints declare honestly.This avoids the failure mode where a strict MUST forces mid-size SSPs (whose webhook pipeline is a Kafka tap, not a warehouse query) to either rebuild their backend or downgrade their webhook to
dailyto dodge the requirement. Honest declaration is the right outcome.Backwards compatibility
windows[]is optional; absent unless requested.windowed_pull_granularities: []— they can only do lifetime/date-range cumulative pulls (matches current behavior).No breaking changes; safe in 3.1 as additive.
Why this matters
Without this, the snapshot/log contract overpromises on data-bearing channels. With this, every channel has either full parity or honest declared limits — buyers know what they can recover from polling vs. what requires reliable webhook delivery.
Combined with #4278 (
webhook_activity[]) which closes the per-fire transport log, the snapshot/log contract becomes: two complete paths within each seller's declared capability, buyer's choice.Out of scope
["hourly", "daily", "monthly", "minutely"]or whatever — but that's a seller-capability question, not a spec gap.reporting_webhook(e.g., conversion event firehose if/when added).Related
Acceptance
get-media-buy-delivery-request.jsongainstime_granularity+include_window_breakdownget-media-buy-delivery-response.jsongainsmedia_buys[].windows[]shapereporting-capabilities.jsongainswindowed_pull_granularitieswindowed_pull_granularities; MAY emit higher-frequency webhooks than they pullUNSUPPORTED_GRANULARITYfor pulls outside declared setsnapshot-and-log.mdxRule 4 SHOULD → MUST promotion for capability-declared granularities