Skip to content

get_media_buy_delivery: windowed-breakdown parity with reporting_webhook frequency #4590

@bokelley

Description

@bokelley

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:

  1. Buyer configures reporting_webhook with reporting_frequency: hourly.
  2. Buyer's webhook receiver is offline for 6 hours.
  3. Six hourly fires fail; transport retries eventually expire.
  4. Buyer recovers their receiver, calls get_media_buy_delivery with the relevant date range.
  5. 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

  • get-media-buy-delivery-request.json gains time_granularity + include_window_breakdown
  • get-media-buy-delivery-response.json gains media_buys[].windows[] shape
  • reporting-capabilities.json gains windowed_pull_granularities
  • Spec text: sellers MUST honor pulls at any granularity in their declared windowed_pull_granularities; MAY emit higher-frequency webhooks than they pull
  • Error code UNSUPPORTED_GRANULARITY for pulls outside declared set
  • snapshot-and-log.mdx Rule 4 SHOULD → MUST promotion for capability-declared granularities
  • Changeset

Metadata

Metadata

Assignees

No one assigned

    Labels

    media-buyIssue concerns the media-buy protocol domainrfcProtocol change — auto-adds to roadmap board

    Type

    No type

    Projects

    Status

    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions