> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-feature-feedback.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Optimization & Reporting

> AdCP optimization and reporting — monitor delivery metrics, analyze dimensional breakdowns, share performance feedback, and optimize active media buys.

Continuous improvement through data-driven monitoring and optimization. AdCP provides comprehensive reporting tools and optimization features to help you track performance, analyze delivery, and improve campaign outcomes.

Reporting in AdCP aligns with the [Targeting](/docs/media-buy/advanced-topics/targeting) system used for campaign setup, enabling consistent analysis across the campaign lifecycle. This unified approach means you can report on exactly what you targeted.

Performance data feeds into AdCP's [Accountability & Trust Framework](/docs/media-buy/index.mdx#accountability--trust-framework), enabling publishers to build reputation through consistent delivery and helping buyers make data-driven allocation decisions.

## Key Optimization Tasks

### Delivery Reporting

Use [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery) to retrieve comprehensive performance data including impressions, spend, clicks, and conversions across all campaign packages.

Alternatively, configure **webhook-based reporting** during media buy creation to receive automated delivery notifications at regular intervals.

### Campaign Updates

Use [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) to modify campaign settings, budgets, and configurations based on performance insights.

## Optimization Workflow

The typical optimization cycle follows this pattern:

1. **Monitor Delivery**: Track campaign performance against targets
2. **Analyze Performance**: Identify optimization opportunities
3. **Make Adjustments**: Update budgets, targeting, or creative assignments
4. **Track Changes**: Monitor impact of optimizations
5. **Iterate**: Continuous improvement through regular analysis

## Metric lifecycle

Every optimization metric in AdCP — whether seller-native (clicks, views, reach), graduated vendor-attested (viewability), or vendor-defined (attention, brand lift, emissions) — flows through the same set of surfaces. Buyers reason about a metric by asking three questions in order:

* **Can you?** — Is the product capable of optimizing for this metric? *(Discovery)*
* **Will you?** — For this package, are you committing to optimize and report it? *(Commitment)*
* **Did you?** — In delivery, what was the value, and did you meet the commitment? *(Reporting)*

Each question maps to a specific protocol surface. The same `metric_id` (or `(vendor, metric_id)` tuple for vendor-attested metrics) flows through every layer — discovery, capability declarations, package commitments, and delivery — so buyers reconciling delivery against a goal never need a translation table.

### Standard metric flow (e.g., `clicks`, `viewable_rate`)

| Question                                           | Surface            | Field                                                         |
| -------------------------------------------------- | ------------------ | ------------------------------------------------------------- |
| Can the product report it?                         | Product capability | `reporting_capabilities.available_metrics`                    |
| Can the product optimize for it?                   | Product capability | `metric_optimization.supported_metrics`                       |
| Will the seller commit to reporting it?            | Package            | `committed_metrics[]` (scope: `standard`)                     |
| Does the buyer want it optimized?                  | Package            | `optimization_goals[]` (kind: `metric`)                       |
| Does the buyer want accountability?                | Package            | `performance_standards[]`                                     |
| What was the value?                                | Delivery           | Standard scalar (e.g., `clicks`, `viewability.viewable_rate`) |
| Did the seller fail to deliver a committed metric? | Delivery           | `missing_metrics[]` (scope: `standard`)                       |

### Vendor-attested metric flow (e.g., Adelaide attention, Scope3 emissions, Kantar brand lift)

Same lifecycle, with the `(vendor, metric_id)` tuple replacing the plain `metric_id` everywhere:

| Question                                           | Surface               | Field                                            |
| -------------------------------------------------- | --------------------- | ------------------------------------------------ |
| Does the vendor define this metric?                | Vendor's capabilities | `get_adcp_capabilities.measurement.metrics[]`    |
| Can the product report this vendor metric?         | Product capability    | `reporting_capabilities.vendor_metrics[]`        |
| Can the product optimize for this vendor metric?   | Product capability    | `vendor_metric_optimization.supported_metrics[]` |
| Will the seller commit to reporting it?            | Package               | `committed_metrics[]` (scope: `vendor`)          |
| Does the buyer want it optimized?                  | Package               | `optimization_goals[]` (kind: `vendor_metric`)   |
| Does the buyer want accountability?                | Package               | `performance_standards[]` (with `vendor` field)  |
| What was the value?                                | Delivery              | `vendor_metric_values[]`                         |
| Did the seller fail to deliver a committed metric? | Delivery              | `missing_metrics[]` (scope: `vendor`)            |

### Why both flows matter

The two flows aren't a duplication — they encode the difference between *standardized* and *vendor-specific* measurement. Clicks mean the same thing across every seller; attention does not. Vendor binding is the wire-level evidence that the buyer and seller agreed on *which* attention model is being optimized and reported. See [`measurement/taxonomy.mdx`](/docs/measurement/taxonomy) for the Tier 0 → Tier 1 graduation policy that decides which flow a given metric uses.

### Required coherence between layers

Three rules connect the layers and prevent orphaned goals:

1. **Optimization requires capability.** A package's `optimization_goals[]` entry MUST match a product capability — `metric_optimization.supported_metrics` for `kind: "metric"`, or `vendor_metric_optimization.supported_metrics` for `kind: "vendor_metric"`. Sellers reject mismatches with `TERMS_REJECTED`.

2. **Optimization requires reporting commitment.** For `kind: "vendor_metric"` goals, the matching `(vendor, metric_id)` MUST also appear in the package's `committed_metrics[]`. Optimizing for a metric the seller isn't committed to reporting is unverifiable — the buyer has no way to grade the goal. Sellers reject uncommitted vendor\_metric goals with `TERMS_REJECTED`. (This rule is normative for vendor metrics specifically; for seller-native `metric` goals, the metric is usually always reported by virtue of being seller-native, so the coherence is implicit.) A third precondition — discovery, that `metric_id` appears in the vendor's published `measurement.metrics[]` catalog — is SHOULD this minor and tightens to MUST at the next minor while measurement-vendor adoption of AdCP-conformant capability publication catches up.

3. **Performance accountability is independent.** `performance_standards[]` lives in parallel — buyers MAY add a threshold commitment on any metric the seller commits to reporting, regardless of whether they also set an optimization goal on it. The three surfaces (goal / commitment / standard) compose:
   * Goal only — "push toward this," no accountability floor
   * Standard only — "you owe me ≥ X," seller decides how to hit it
   * Both — optimization steers; standard backstops

4. **Priority is ordinal, lowest-value-first.** When `optimization_goals[]` carry explicit `priority` values, the seller treats the goal with the **lowest** priority value as primary — even when no goal is `priority: 1` (priorities of `2` and `3` mean `2` is primary). When `priority` is omitted, sellers may use array position. Duplicate priority values are undefined.

### Worked example: Adelaide attention end-to-end

A buyer wants to optimize against Adelaide's `attention_score` with a threshold of 70. Here's how the four surfaces line up:

**1. Product capability** (discovered via `get_products`):

```json theme={null}
{
  "product_id": "premium_ctv_video",
  "vendor_metric_optimization": {
    "supported_metrics": [
      {
        "vendor": { "domain": "adelaidemetrics.com" },
        "metric_id": "attention_score",
        "supported_targets": ["cost_per", "threshold_rate"]
      }
    ]
  },
  "reporting_capabilities": {
    "vendor_metrics": [
      { "vendor": { "domain": "adelaidemetrics.com" }, "metric_id": "attention_score" }
    ]
  }
}
```

**2. Package proposal on `create_media_buy`** — the buyer commits to reporting AND sets the optimization goal (both required by the reporting-coherence rule):

```json theme={null}
{
  "product_id": "premium_ctv_video",
  "committed_metrics": [
    {
      "scope": "vendor",
      "vendor": { "domain": "adelaidemetrics.com" },
      "metric_id": "attention_score"
    }
  ],
  "optimization_goals": [
    {
      "kind": "vendor_metric",
      "vendor": { "domain": "adelaidemetrics.com" },
      "metric_id": "attention_score",
      "target": { "kind": "threshold_rate", "value": 70 },
      "priority": 1
    }
  ]
}
```

A `performance_standard` MAY be layered on top if the buyer wants a contractual floor — e.g., `{ "metric": "attention_score", "vendor": { "domain": "adelaidemetrics.com" }, "threshold": 65 }` — but it's independent of the goal.

**3. Delivery** (`get_media_buy_delivery` response):

```json theme={null}
{
  "vendor_metric_values": [
    {
      "vendor": { "domain": "adelaidemetrics.com" },
      "metric_id": "attention_score",
      "value": 73.2,
      "measurable_impressions": 420000
    }
  ]
}
```

The seller's bidding stack steered toward higher Adelaide attention scores; the reported value `73.2` is above the threshold `70`, so the goal is met. The `measurable_impressions` denominator (`420000` out of, say, 1M delivered impressions) is the coverage figure for Adelaide's measurement — vendor SDKs rarely fire on 100% of delivered impressions, and attention-vendor coverage on CTV today is typically 30–60% depending on app-SDK presence. Always compute coverage from `measurable_impressions / impressions` before reasoning about the reported value.

**4. Accountability gap reporting** (also in the `get_media_buy_delivery` response, when commitment isn't met):

```json theme={null}
{
  "by_package": [{
    "package_id": "...",
    "missing_metrics": [
      {
        "scope": "vendor",
        "vendor": { "domain": "adelaidemetrics.com" },
        "metric_id": "attention_score"
      }
    ]
  }]
}
```

If Adelaide's coverage on the inventory dropped below the seller's confidence threshold and they couldn't populate `vendor_metric_values` for this period, the same `(vendor, metric_id)` shows up in `missing_metrics` — same key across the entire lifecycle, so buyer reconciliation is a row-level join.

## Performance Monitoring

### Real-Time Metrics

Track campaign delivery as it happens:

* **Impression delivery** vs. targets
* **Spend pacing** against budget
* **Click-through rates** and engagement
* **Conversion tracking** for business outcomes

### Historical Analysis

Understand performance trends over time:

* **Daily/hourly breakdowns** of key metrics
* **Performance comparisons** across time periods
* **Trend identification** for optimization opportunities

### Alerting and Notifications

Stay informed of important campaign events:

* **Delivery alerts** for pacing issues
* **Performance notifications** for significant changes
* **Budget warnings** before limits are reached

## Delivery Methods

Publishers can proactively push reporting data to buyers through webhook notifications or offline file delivery. This eliminates continuous polling and provides timely campaign insights.

**Webhook Push (Real-time)** - HTTP POST to buyer endpoint

* Best for: Most buyer-seller relationships
* Latency: Near real-time (seconds to minutes)
* Cost: Standard webhook infrastructure

**Offline File Delivery (Batch)** - Cloud storage bucket push

* Best for: Large buyer-seller pairs (high volume)
* Latency: Scheduled batch delivery (hourly/daily)
* Cost: Significantly lower (\$0.01-0.10 per GB vs. \$0.50-2.00 per 1M webhooks)
* Format: JSON Lines, CSV, or Parquet files
* Storage: S3, GCS, Azure Blob Storage

### Webhook-Based Reporting

#### Webhook Configuration

Configure reporting webhooks when creating a media buy using the `reporting_webhook` parameter:

```json theme={null}
{
  "packages": [...],
  "reporting_webhook": {
    "url": "https://buyer.example.com/webhooks/reporting",
    "authentication": {
      "schemes": ["Bearer"],
      "credentials": "secret_token_min_32_chars"
    },
    "reporting_frequency": "daily"
  }
}
```

**Or with HMAC signature (recommended for production):**

```json theme={null}
{
  "packages": [...],
  "reporting_webhook": {
    "url": "https://buyer.example.com/webhooks/reporting",
    "authentication": {
      "schemes": ["HMAC-SHA256"],
      "credentials": "shared_secret_min_32_chars"
    },
    "reporting_frequency": "daily"
  }
}
```

**Security is Required:**

* `authentication` configuration is mandatory (minimum 32 characters)
* **Bearer tokens**: Simple, good for development (Authorization header)
* **HMAC-SHA256**: Production-recommended, prevents replay attacks (signature headers)
* Credentials exchanged out-of-band during publisher onboarding
* See [Security](/docs/building/implementation/security) for implementation details

#### Supported Frequencies

Publishers declare supported reporting frequencies in the product's `reporting_capabilities`. Publishers are **not required** to support all frequencies - choose what makes operational sense for your platform.

* **`hourly`**: Receive notifications every hour during campaign flight (optional, consider cost/complexity)
* **`daily`**: Receive notifications once per day (most common, recommended for Phase 1)
* **`monthly`**: Receive notifications once per month (timezone specified by publisher)

**Cost Consideration:** Hourly webhooks generate 24x more traffic than daily. Large buyer-seller pairs may prefer offline reporting mechanisms (see below) for cost efficiency.

#### Available Metrics

Metric availability is declared at two levels:

1. **Product level**: `reporting_capabilities.available_metrics` declares what the platform can report
2. **Format level**: `reported_metrics` on a creative format declares what the format can produce (see [Reported Metrics](/docs/creative/formats#reported-metrics))

Buyers receive the intersection of both. `impressions` and `spend` are always reported regardless of the intersection. Standard metrics include:

* **`impressions`**: Ad views (always available)
* **`spend`**: Amount spent (always available)
* **`clicks`**: Click events
* **`ctr`**: Click-through rate
* **`views`**: Views at platform-defined threshold
* **`completed_views`**: Video/audio completions (or threshold-based completion when an optimization goal sets a custom view duration)
* **`completion_rate`**: Completion rate (`completed_views` / `impressions`)
* **`conversions`**: Post-click or post-view conversions
* **`conversion_value`**: Monetary value of attributed conversions
* **`roas`**: Return on ad spend
* **`cost_per_acquisition`**: Cost per conversion
* **`new_to_brand_rate`**: Fraction of conversions from first-time brand buyers
* **`leads`**: Leads generated
* **`reach`**: Unique reach (paired with `reach_unit`). Measurement window declared via `reach_window` (`cumulative` / `period` / `rolling`). When `reach_window` is omitted, the window is unspecified — buyers MUST NOT sum reach across rows.
* **`reach_window`**: Window semantics for reach/frequency — object with `kind` (`cumulative` since campaign start, `period` for non-overlapping snapshot, `rolling` for trailing window) and `period: Duration` (required for `period` and `rolling`)
* **`frequency`**: Average frequency per reach unit, measured over `reach_window`
* **`grps`**: Gross Rating Points (for CPP billing)
* **`engagements`**: Direct ad interactions beyond viewing (reactions, taps, opens)
* **`engagement_rate`**: Platform-specific engagement rate
* **`follows`**: New followers, page likes, artist/podcast/channel follows, or free channel/feed subscribes attributed to delivery. Paid subscriptions are conversion events with `event_type: "subscribe"`.
* **`saves`**: Saves, bookmarks, playlist adds attributed to delivery (platforms vary in name — Pinterest "repins", TikTok "video\_saves" — all report under this canonical key)
* **`profile_visits`**: Visits to the brand's in-platform page
* **`viewability`**: Viewability data (measurable\_impressions, viewable\_impressions, viewable\_rate, viewed\_seconds, standard, vendor). Separates MRC and GroupM standards. `viewed_seconds` is the average in-view duration per measurable impression — reporting-side counterpart to the `viewed_seconds` optimization goal, governed by the same `standard` threshold as `viewable_rate`. The optional `vendor` field carries a `BrandRef` so the row is self-describing — buyer agents reading delivery in isolation can attribute the numbers to a measurement vendor without joining back to `package.committed_metrics` or `package.performance_standards`.
* **`quartile_data`**: Video quartile completion data (q1-q4)
* **`dooh_metrics`**: DOOH-specific metrics (loop plays, screens, venue breakdown)
* **`cost_per_click`**: Cost per click (`spend / clicks`)
* **`cost_per_completed_view`**: Cost per completed view (`spend / completed_views`); CPCV pricing scalar for video/audio inventory
* **`cpm`**: Cost per thousand impressions (`(spend / impressions) × 1000`); universal pricing scalar across CTV, display, mobile/web video, native, audio, and DOOH
* **`downloads`**: Audio/podcast downloads (IAB Podcast Measurement Technical Guidelines methodology); distinct from `views`
* **`units_sold`**: Items sold attributed to delivery (retail-media commerce scalar; distinct from `conversions` — one transaction may carry multiple units; attribution-window declared via `measurement_terms`)
* **`new_to_brand_units`**: Unit-volume parallel to `new_to_brand_rate` — count of units sold to first-time brand buyers
* **`plays`**: Raw play count for DOOH/broadcast inventory (mirrors `forecastable-metric.plays`); distinct from `dooh_metrics.loop_plays` (per-screen rotation) and `impressions` (multiplied audience figure)

Buyers can optionally request a subset via `requested_metrics` to reduce payload size and focus on relevant KPIs.

#### Vendor-Defined Metrics

The standard `available_metrics` enum is the closed protocol vocabulary. Vendor-defined metrics — proprietary attention scores, emissions per impression, panel-based demographics, brand-lift surveys, in-flight attention panels, custom viewability variants — live in a parallel structured surface:

* **Declaration** (`reporting_capabilities.vendor_metrics`): each entry is a pointer (`{ vendor: BrandRef, metric_id }`) into the vendor's metric catalog. Sellers say "I support this vendor's metric"; everything else (category, methodology, standard alignment, human-readable documentation) lives at the vendor. Identifiers are namespaced by the vendor — the same `metric_id` may mean different things in different vendors' vocabularies.

* **Discovery anchor**: the vendor's `brand.json` `agents[type='measurement']` is the canonical place to find the measurement agent's URL, capability profile, the methodology, the standards each metric implements, and per-metric documentation. AdCP does not duplicate that metadata on every seller's per-product extension; buyers resolve it once per vendor (cacheable) when they need it.

* **Filter** (`get_products` `filters.required_vendor_metrics`): each entry pins `vendor` and/or `metric_id` — at least one. Cross-vendor queries (e.g., "any attention measurement from a vendor that supports it") are the buyer agent's responsibility: the agent resolves which vendors offer a category via their `brand.json` records, then enumerates them as filter entries. Same filter-not-fail convention as the other `required_*` filters.

* **Reporting** (`vendor_metric_values` on each `by_package` delivery row): each value carries `{ vendor, metric_id, value, unit?, measurable_impressions?, breakdown? }`. `measurable_impressions` is the coverage denominator — vendor measurement is rarely 100% of delivered impressions. When the field is present, buyers compute coverage as `measurable_impressions / impressions`; when absent, coverage is unspecified (do not compute a rate or assume full coverage). The `breakdown` slot is the place vendors put structured payloads beyond a single scalar (panel demographics, co-view ratios, incremental decompositions); it is the only escape hatch — the rest of the value envelope is closed.

* **Promotion path**: when the industry converges on a metric via a published standard, the spec adds it to the closed `available_metrics` enum and the vendor extensions become historical aliases. Promotion is anchored on standards-body publication, not on ad-hoc vendor convergence counts.

* **Accountability scope**: when the seller stamps a vendor metric in `package.committed_metrics` (with `scope: "vendor"`), it is subject to the same `missing_metrics` contract on `get_media_buy_delivery` as standard metrics. Sellers who can't credibly attest to a vendor metric SHOULD NOT stamp it in `committed_metrics`; absence keeps the metric advisory and reconciliation falls back to `vendor_metric_values.measurable_impressions` coverage plus out-of-band verification via the vendor's measurement agent. The advisory-vs-accountable distinction is now explicit at the contract layer rather than asymmetric across metric scopes.

#### Publisher Commitment

When a reporting webhook is configured, publishers commit to sending:

**(campaign\_duration / reporting\_frequency) + 1** notifications

* One notification per frequency period during the campaign
* One final notification when the campaign completes
* If reporting data is delayed beyond the expected delay window, a `"delayed"` notification will be sent

#### Webhook Payload

Reporting webhooks send a full MCP webhook envelope. The delivery report itself uses the same payload structure as [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery) with additional metadata, but it is nested under `result`. Do not send the inner delivery object as the top-level POST body.

```json theme={null}
{
  "idempotency_key": "whk_20240205_example_000005",
  "operation_id": "delivery_report_mb001_2024_02_05",
  "task_id": "delivery_report_mb001_2024_02_05_000005",
  "task_type": "media_buy_delivery",
  "status": "completed",
  "timestamp": "2024-02-06T08:00:00Z",
  "message": "Scheduled media buy delivery report available",
  "result": {
    "notification_type": "scheduled",
    "sequence_number": 5,
    "next_expected_at": "2024-02-06T08:00:00Z",
    "reporting_period": {
      "start": "2024-02-05T00:00:00Z",
      "end": "2024-02-05T23:59:59Z"
    },
    "currency": "USD",
    "media_buy_deliveries": [
      {
        "media_buy_id": "mb_001",
        "status": "active",
        "totals": {
          "impressions": 125000,
          "spend": 5625.0,
          "clicks": 250,
          "ctr": 0.002
        },
        "by_package": []
      }
    ]
  }
}
```

The inner `result` object can be shown or stored separately, but this shape is not valid as the top-level webhook POST body:

```json theme={null}
{
  "notification_type": "scheduled",
  "sequence_number": 5,
  "reporting_period": {
    "start": "2024-02-05T00:00:00Z",
    "end": "2024-02-05T23:59:59Z"
  },
  "currency": "USD",
  "media_buy_deliveries": [
    {
      "media_buy_id": "mb_001",
      "status": "active",
      "totals": {
        "impressions": 125000,
        "spend": 5625.0
      },
      "by_package": []
    }
  ]
}
```

**Fields:**

* **`idempotency_key`**: Unique per webhook event and stable across retries of that same event. Receivers dedupe by this field.
* **Top-level `status`**: Async webhook status, such as `"completed"` for a delivered report.
* **`notification_type`**: `"scheduled"` (regular update), `"final"` (campaign complete), or `"delayed"` (data not yet available)
* **`sequence_number`**: Sequential notification number (starts at 1)
* **`next_expected_at`**: ISO 8601 timestamp for next notification (omitted for final notifications)
* **`media_buy_deliveries[].status`**: Media buy lifecycle or reporting status, distinct from the top-level async webhook status.
* **`media_buy_deliveries`**: Array of media buy delivery data (may contain multiple media buys aggregated by publisher)

Webhook signatures and content digests are computed over the exact raw bytes of the full envelope being sent, not over a reserialized `result` object.

#### Timezone Handling

**All reporting MUST use UTC.** This eliminates DST complexity, simplifies reconciliation, and ensures consistent 24-hour reporting periods.

```json theme={null}
{
  "reporting_capabilities": {
    "timezone": "UTC",
    "available_reporting_frequencies": ["daily"],
    "date_range_support": "date_range"
  }
}
```

**Reporting periods:**

* Daily: 00:00:00Z to 23:59:59Z (always 24 hours)
* Hourly: Top of hour to 59:59 seconds (always 1 hour)
* Monthly: First to last day of month

**Example webhook payload:**

```json theme={null}
{
  "reporting_period": {
    "start": "2024-02-05T00:00:00Z",
    "end": "2024-02-05T23:59:59Z"
  }
}
```

#### Delayed Reporting

If reporting data is not available within the product's `expected_delay_minutes`, publishers send a notification with `notification_type: "delayed"`:

```json theme={null}
{
  "notification_type": "delayed",
  "sequence_number": 3,
  "next_expected_at": "2024-02-06T10:00:00Z",
  "message": "Reporting data delayed due to upstream processing. Expected availability in 2 hours."
}
```

This prevents buyers from incorrectly assuming a missed notification.

#### Measurement Maturation Windows

For channels where billing-grade data is produced in **phases** rather than arriving final on day one, sellers declare `measurement_windows` on their products. Each window describes a maturation stage with its own expected availability. This pattern is used across channels:

| Channel                    | Typical windows                                                                  |
| -------------------------- | -------------------------------------------------------------------------------- |
| Broadcast / linear TV      | `live` (same day) → `c3` (\~4 days) → `c7` (\~15–22 days, guarantee basis)       |
| DOOH                       | `tentative` (same day) → `final` post-IVT/fraud-check (\~1 day, guarantee basis) |
| Digital with IVT filtering | raw → `post_givt` → `post_sivt` (\~2–3 days, guarantee basis)                    |
| Podcast                    | `downloads_7d` → `downloads_30d` (guarantee basis)                               |

Each window's numbers supersede the previous one. One window is typically the `is_guarantee_basis` — the number both sides reconcile against. The measurement vendor's processing time is captured in `expected_availability_days` on each window (it includes both accumulation and processing).

Sellers set `expected_delay_minutes` on their products to reflect the first-available data pipeline. The `measurement_windows` array on `reporting_capabilities` provides per-window timelines.

Delivery data for products with measurement windows includes three fields on each package:

* **`is_final`** — `true` when the seller considers this data closed for the reporting period. `false` when the data will be updated (wider window, more processing). Absent when the seller doesn't distinguish provisional from final.
* **`measurement_window`** — which window this data represents (e.g., `"c3"`). References a `window_id` from the product's `measurement_windows`. Absent for standard digital reporting without windowed maturation.
* **`supersedes_window`** — which previous window this report replaces (e.g., `"live"` when C3 data arrives). Absent on the first report for a period.

When the seller sends updated data for the same period with a wider window, they use `notification_type: "window_update"`. This is distinct from `adjusted` (a correction within the same window).

**Measurement window lifecycle example** — a broadcast spot airing March 1:

**March 2** — Live data arrives (notification\_type: `scheduled`):

```json theme={null}
{
  "notification_type": "scheduled",
  "media_buy_deliveries": [{
    "media_buy_id": "mb_nova_q4",
    "by_package": [{
      "package_id": "primetime_30s",
      "impressions": 980000,
      "spend": 24500,
      "is_final": false,
      "measurement_window": "live"
    }]
  }]
}
```

**March 5** — C3 data supersedes live (notification\_type: `window_update`):

```json theme={null}
{
  "notification_type": "window_update",
  "media_buy_deliveries": [{
    "media_buy_id": "mb_nova_q4",
    "by_package": [{
      "package_id": "primetime_30s",
      "impressions": 1050000,
      "spend": 26250,
      "is_final": false,
      "measurement_window": "c3",
      "supersedes_window": "live"
    }]
  }]
}
```

**March 16** — C7 data arrives, final for this period (notification\_type: `window_update`):

```json theme={null}
{
  "notification_type": "window_update",
  "media_buy_deliveries": [{
    "media_buy_id": "mb_nova_q4",
    "by_package": [{
      "package_id": "primetime_30s",
      "impressions": 1120000,
      "spend": 28000,
      "is_final": true,
      "measurement_window": "c7",
      "supersedes_window": "c3"
    }]
  }]
}
```

The buyer replaces stored data each time a `window_update` arrives. When `is_final: true`, this is the number to reconcile against the guarantee. The same lifecycle shape applies to DOOH (`tentative` → `final`), digital with IVT filtering (`post_givt` → `post_sivt`), podcast (`downloads_7d` → `downloads_30d`), and any other channel where data matures in phases — only the window IDs and timing differ.

For how `measurement_window` appears on billing terms and drives reconciliation and invoicing clocks, see [Accountability](/docs/media-buy/advanced-topics/accountability).

#### Webhook Aggregation

Publishers SHOULD aggregate webhooks to reduce call volume when multiple media buys share:

* Same webhook URL
* Same reporting frequency
* Same reporting period

**Example**: Buyer has 100 active campaigns with daily reporting to the same endpoint. Publisher sends:

* **Without aggregation**: 100 webhooks per day (inefficient)
* **With aggregation**: 1 webhook per day containing all 100 campaigns (optimal)

The `media_buy_deliveries` array may contain 1 to N media buys per webhook. Buyers should iterate through the array to process each campaign's data.

**Aggregated webhook example:**

```json theme={null}
{
  "notification_type": "scheduled",
  "reporting_period": {
    "start": "2024-02-05T00:00:00Z",
    "end": "2024-02-05T23:59:59Z"
  },
  "currency": "USD",
  "media_buy_deliveries": [
    { "media_buy_id": "mb_001", "totals": { "impressions": 50000, "spend": 1750 }, ... },
    { "media_buy_id": "mb_002", "totals": { "impressions": 48500, "spend": 1695 }, ... },
    // ... 98 more media buys
  ]
}
```

Buyers should iterate through the array and process each media buy independently. If aggregated totals are needed, calculate them from the individual media buy totals.

#### Partial Failure Handling

When aggregating multiple media buys into a single webhook, publishers must handle cases where some campaigns have data available while others don't.

**Approach: Best-Effort Delivery with Status Indicators**

Publishers SHOULD send aggregated webhooks containing all available data, using status fields to indicate partial availability:

```json theme={null}
{
  "notification_type": "scheduled",
  "sequence_number": 5,
  "reporting_period": {
    "start": "2024-02-05T00:00:00Z",
    "end": "2024-02-05T23:59:59Z"
  },
  "currency": "USD",
  "media_buy_deliveries": [
    {
      "media_buy_id": "mb_001",
      "status": "active",
      "totals": {
        "impressions": 50000,
        "spend": 1750
      }
    },
    {
      "media_buy_id": "mb_002",
      "status": "active",
      "totals": {
        "impressions": 48500,
        "spend": 1695
      }
    },
    {
      "media_buy_id": "mb_003",
      "status": "reporting_delayed",
      "message": "Reporting data temporarily unavailable for this campaign",
      "expected_availability": "2024-02-06T02:00:00Z"
    }
  ],
  "partial_data": true,
  "unavailable_count": 1
}
```

**Key Fields for Partial Failures:**

* `partial_data`: Boolean indicating if any campaigns are missing data
* `unavailable_count`: Number of campaigns with delayed/missing data
* `status`: Per-campaign status (`"active"`, `"reporting_delayed"`, `"failed"`)
* `expected_availability`: When delayed data is expected (if known)

**When to Use Partial Delivery:**

1. **Upstream delays**: Some data sources are slower than others
2. **System degradation**: Partial system outage affects subset of campaigns
3. **Data quality issues**: Specific campaigns fail validation, others proceed
4. **Rate limiting**: API limits prevent fetching all campaign data

**When NOT to Use Partial Delivery:**

1. **Complete system outage**: Send `"delayed"` notification instead
2. **All campaigns affected**: Use `notification_type: "delayed"`
3. **Buyer endpoint issues**: Circuit breaker handles this (don't send at all)

**Buyer Processing Logic:**

```javascript theme={null}
function processAggregatedWebhook(webhook) {
  if (webhook.partial_data) {
    console.warn(`Partial data: ${webhook.unavailable_count} campaigns delayed`);
  }

  for (const delivery of webhook.media_buy_deliveries) {
    if (delivery.status === 'reporting_delayed') {
      // Mark campaign as pending, retry via polling or wait for next webhook
      markCampaignPending(delivery.media_buy_id, delivery.expected_availability);
    } else if (delivery.status === 'active') {
      // Process normal delivery data
      processCampaignMetrics(delivery);
    } else {
      console.error(`Unexpected status for ${delivery.media_buy_id}: ${delivery.status}`);
    }
  }
}
```

**Best Practices:**

* Always include all campaigns in array, even if data unavailable (with status indicator)
* Set `partial_data: true` flag when any campaigns are delayed/failed
* Provide `expected_availability` timestamp if known
* Don't retry the entire webhook - buyers can poll individual campaigns if needed
* Track partial delivery rates in monitoring to detect systemic issues

#### Privacy and Compliance

##### PII Scrubbing for GDPR/CCPA

Publishers MUST scrub personally identifiable information (PII) from all webhook payloads to ensure GDPR and CCPA compliance. Reporting webhooks should contain only aggregated, anonymized metrics.

**What to Scrub:**

* User IDs, device IDs, IP addresses
* Email addresses, phone numbers
* Precise geolocation data (latitude/longitude)
* Cookie IDs, advertising IDs (unless aggregated)
* Any custom dimensions containing PII

**What to Keep:**

* Aggregated metrics (impressions, spend, clicks, etc.)
* Coarse geography (city, state, country - not street address)
* Device type categories (mobile, desktop, tablet)
* Browser/OS categories
* Time-based aggregations

**Example - Before PII Scrubbing (❌ DO NOT SEND):**

```json theme={null}
{
  "media_buy_id": "mb_001",
  "user_events": [
    {
      "user_id": "user_12345",
      "ip_address": "192.168.1.100",
      "device_id": "abc-def-ghi",
      "impressions": 1,
      "lat": 40.7128,
      "lon": -74.0060
    }
  ]
}
```

**Example - After PII Scrubbing (✅ CORRECT):**

```json theme={null}
{
  "media_buy_id": "mb_001",
  "totals": {
    "impressions": 125000,
    "spend": 5625.00,
    "clicks": 250
  },
  "by_package": [
    {
      "package_id": "pkg_001",
      "impressions": 125000,
      "spend": 5625.00,
      "by_geo": [
        {
          "geo_level": "region",
          "geo_code": "US-NY",
          "geo_name": "New York",
          "impressions": 45000,
          "spend": 2025.00
        }
      ],
      "by_geo_truncated": false
    }
  ]
}
```

**Publisher Responsibilities:**

* Implement PII scrubbing at the data collection layer, not at webhook delivery
* Ensure aggregation thresholds prevent re-identification (e.g., minimum 10 users per segment)
* Document what data is collected vs. what is shared in webhooks
* Provide data processing agreements (DPAs) for GDPR compliance
* Support GDPR/CCPA data deletion requests

**Buyer Responsibilities:**

* Do not request PII in `requested_metrics` or custom dimensions
* Understand that webhook data is aggregated and anonymized
* Implement proper data retention policies
* Include webhook data in privacy policies and user disclosures

#### Implementation Best Practices

1. **Handle Arrays**: Always process `media_buy_deliveries` as an array, even if it contains one element
2. **Idempotent Handlers**: Process duplicate notifications safely (webhooks use at-least-once delivery)
3. **Sequence Tracking**: Use `sequence_number` to detect missing or out-of-order notifications
4. **Fallback Polling**: Continue periodic polling as backup if webhooks fail
5. **Timezone Awareness**: Store publisher's reporting timezone for accurate period calculation
6. **Validate Frequency**: Ensure requested frequency is in product's `available_reporting_frequencies`
7. **Validate Metrics**: Ensure requested metrics are in product's `available_metrics`
8. **PII Compliance**: Never include user-level data in webhook payloads

#### Webhook Health Monitoring

Webhook delivery status is tracked through **AdCP's global task management system** (see [Task Lifecycle](/docs/building/implementation/task-lifecycle)).

When a media buy is created with `reporting_webhook` configured, the publisher creates an ongoing task for webhook delivery. Buyers can monitor webhook health using standard task queries.

**Benefits of using task management:**

* Consistent status tracking across all AdCP operations
* Standard polling/webhook notification patterns
* Existing infrastructure for task status, history, and errors
* No need for media-buy-specific webhook health endpoints

If webhook delivery fails persistently (circuit breaker opens), publishers update the task status to indicate the issue. Buyers detect this through normal task monitoring.

### Offline-File-Delivery-Based Reporting

**Example: Offline Delivery**
Publisher pushes daily report files to buyer's cloud storage:

```
s3://buyer-reports/publisher_name/2024/02/05/media_buy_delivery.json.gz
```

File contains same structure as webhook payload but aggregated across all campaigns. Buyer processes files on their schedule.

**When to Use Offline Delivery:**

* \>100 active campaigns with same buyer
* Hourly reporting requirements (24x cost reduction)
* High data volume (detailed breakdowns, dimensional data)
* Buyer has batch processing infrastructure

Sellers declare their supported push-based delivery methods and protocols in `get_adcp_capabilities`. Polling via `get_media_buy_delivery` is always available — it is a required task for all `media_buy` sellers.

```json theme={null}
{
  "media_buy": {
    "reporting_delivery_methods": ["webhook", "offline"],
    "offline_delivery_protocols": ["s3", "gcs"]
  }
}
```

Buyers express a protocol preference when syncing accounts. The seller provisions the bucket using the preferred protocol if supported:

```json theme={null}
{
  "accounts": [{
    "brand": { "domain": "nova-brands.com" },
    "operator": "pinnacle-media.com",
    "billing": "operator",
    "preferred_reporting_protocol": "s3"
  }]
}
```

Products declare supported cadence and metrics in `reporting_capabilities`:

```json theme={null}
{
  "reporting_capabilities": {
    "available_reporting_frequencies": ["daily"],
    "supports_webhooks": true,
    "available_metrics": ["impressions", "spend", "clicks"],
    "date_range_support": "date_range"
  }
}
```

For offline delivery, the seller provisions storage per account and grants the buyer read access out-of-band. Sellers may use a dedicated bucket per account or a shared bucket with per-account `prefix` isolation — either way, the buyer can only access data under their account's path. When multiple buying platforms operate on the same brand, each gets a separate account (scoped by operator/agent), so their data is isolated by prefix.

The bucket location appears on the account object returned by `sync_accounts`:

```json theme={null}
{
  "account_id": "acc_pinnacle_001",
  "status": "active",
  "reporting_bucket": {
    "protocol": "s3",
    "bucket": "seller-reports",
    "prefix": "accounts/pinnacle/adcp",
    "region": "us-east-1",
    "format": "jsonl",
    "compression": "gzip",
    "file_retention_days": 30,
    "setup_instructions": "https://seller.example.com/docs/bucket-access"
  }
}
```

Buyers read from the bucket on their own schedule. The seller pushes files at the product's reporting frequency.

**Delivery method determines the reporting path:**

* `get_media_buy_delivery` is a required task for all `media_buy` sellers. Polling is always available as a baseline, regardless of which push methods the seller supports.
* When `reporting_delivery_methods` includes `offline` and a `reporting_bucket` is present on the account, the seller also pushes detailed delivery data to the bucket. Buyers with batch infrastructure should read from the bucket for efficiency.
* Files in the bucket are retained for `file_retention_days` (declared on the `reporting_bucket`). Buyers must read files within this window.
* `get_media_buys` always returns the media buy object with status, totals, and pacing snapshots. Use it for status checks, not detailed reporting.

For offline file delivery, publishers can provide reporting data in JSON Lines (JSONL), CSV, Parquet, Avro, or ORC format. All formats preserve the nested JSON structure from webhook payloads, making them ideal for batch processing.

JSONL and CSV files can be compressed with gzip (`.jsonl.gz`, `.csv.gz`) to reduce storage and transfer costs. Parquet, Avro, and ORC use internal compression — the top-level `compression` field is ignored for these formats.

#### JSON Lines (JSONL)

One media buy delivery per line (newline-delimited JSON). Each line contains a single media buy delivery object with its reporting period and package-level data. Preserves full nested structure. Easy to parse line-by-line for streaming processing.

**Example JSONL file:**

```jsonl theme={null}
{"notification_type": "scheduled", "sequence_number": 5, "next_expected_at": "2024-02-06T08:00:00Z", "reporting_period": {"start": "2024-02-05T00:00:00Z", "end": "2024-02-05T23:59:59Z"}, "currency": "USD", "media_buy_id": "mb_001", "status": "active", "totals": {"impressions": 50000, "spend": 1750.00, "clicks": 100, "ctr": 0.002}, "by_package": [{"package_id": "pkg_001", "impressions": 30000, "spend": 1050.00, "pacing_index": 0.95, "pricing_model": "cpm", "rate": 0.035, "currency": "USD"}, {"package_id": "pkg_002", "impressions": 20000, "spend": 700.00, "pacing_index": 0.98, "pricing_model": "cpm", "rate": 0.035, "currency": "USD"}]}
{"notification_type": "scheduled", "sequence_number": 5, "next_expected_at": "2024-02-06T08:00:00Z", "reporting_period": {"start": "2024-02-05T00:00:00Z", "end": "2024-02-05T23:59:59Z"}, "currency": "USD", "media_buy_id": "mb_002", "status": "active", "totals": {"impressions": 200000, "spend": 9000.00, "clicks": 400, "ctr": 0.002}, "by_package": [{"package_id": "pkg_003", "impressions": 200000, "spend": 9000.00, "pacing_index": 1.02, "pricing_model": "cpm", "rate": 45.00, "currency": "USD"}]}
{"notification_type": "scheduled", "sequence_number": 5, "next_expected_at": "2024-02-06T08:00:00Z", "reporting_period": {"start": "2024-02-05T00:00:00Z", "end": "2024-02-05T23:59:59Z"}, "currency": "USD", "media_buy_id": "mb_003", "status": "active", "totals": {"impressions": 75000, "spend": 3375.00, "clicks": 150, "ctr": 0.002}, "by_package": [{"package_id": "pkg_004", "impressions": 75000, "spend": 3375.00, "pacing_index": 0.96, "pricing_model": "cpcv", "rate": 0.045, "currency": "USD"}]}
```

#### CSV

**For tabular analysis**

CSV files require unnesting nested arrays. Each record should be unnested to the `by_package` level, meaning one row per package with parent-level data (reporting period, media buy info, totals) duplicated.

**Example CSV structure:**

```csv theme={null}
notification_type,sequence_number,next_expected_at,reporting_period_start,reporting_period_end,currency,media_buy_id,status,totals_impressions,totals_spend,totals_clicks,totals_ctr,by_package_package_id,by_package_impressions,by_package_spend,by_package_clicks,by_package_pacing_index,by_package_pricing_model,by_package_rate,by_package_currency
scheduled,5,2024-02-06T08:00:00Z,2024-02-05T00:00:00Z,2024-02-05T23:59:59Z,USD,mb_001,active,50000,1750.00,100,0.002,pkg_001,30000,1050.00,60,0.95,cpm,0.035,USD
scheduled,5,2024-02-06T08:00:00Z,2024-02-05T00:00:00Z,2024-02-05T23:59:59Z,USD,mb_001,active,50000,1750.00,100,0.002,pkg_002,20000,700.00,40,0.98,cpm,0.035,USD
scheduled,5,2024-02-06T08:00:00Z,2024-02-05T00:00:00Z,2024-02-05T23:59:59Z,USD,mb_002,active,200000,9000.00,400,0.002,pkg_003,200000,9000.00,400,1.02,cpm,45.00,USD
scheduled,5,2024-02-06T08:00:00Z,2024-02-05T00:00:00Z,2024-02-05T23:59:59Z,USD,mb_003,active,75000,3375.00,150,0.002,pkg_004,75000,3375.00,150,0.96,cpcv,0.045,USD
```

#### Parquet

**For high-volume analytics**

Columnar format optimized for analytics workloads. Excellent compression ratios. Supports nested structures natively. Best for data warehouses and big data processing.

**Example Parquet schema:**

```json theme={null}
{
  "type": "record",
  "name": "MediaBuyDelivery",
  "fields": [
    {"name": "notification_type", "type": "string"},
    {"name": "sequence_number", "type": "int"},
    {"name": "next_expected_at", "type": "string"},
    {"name": "reporting_period", "type": {
      "type": "record",
      "name": "ReportingPeriod",
      "fields": [
        {"name": "start", "type": "string"},
        {"name": "end", "type": "string"}
      ]
    }},
    {"name": "currency", "type": "string"},
    {"name": "media_buy_id", "type": "string"},
    {"name": "status", "type": "string"},
    {"name": "totals", "type": {
      "type": "record",
      "name": "Totals",
      "fields": [
        {"name": "impressions", "type": "long"},
        {"name": "spend", "type": "double"},
        {"name": "clicks", "type": "long"},
        {"name": "ctr", "type": "double"}
      ]
    }},
    {"name": "by_package", "type": {
      "type": "array",
      "items": {
        "type": "record",
        "name": "PackageDelivery",
        "fields": [
          {"name": "package_id", "type": "string"},
          {"name": "impressions", "type": "long"},
          {"name": "spend", "type": "double"},
          {"name": "pacing_index", "type": "double"},
          {"name": "pricing_model", "type": "string"},
          {"name": "rate", "type": "double"},
          {"name": "currency", "type": "string"}
        ]
      }
    }}
  ]
}
```

#### Avro

**For schema-rich streaming pipelines**

Row-oriented format with embedded schema. Self-describing — readers don't need external schema files. Handles schema evolution (adding/removing fields) gracefully. Common in Kafka and Hadoop ecosystems. Uses internal compression (snappy, deflate, or zstd).

**Example Avro schema:**

```json theme={null}
{
  "type": "record",
  "name": "MediaBuyDelivery",
  "namespace": "org.example.reporting",
  "fields": [
    {"name": "notification_type", "type": "string"},
    {"name": "sequence_number", "type": "int"},
    {"name": "next_expected_at", "type": "string"},
    {"name": "reporting_period", "type": {
      "type": "record",
      "name": "ReportingPeriod",
      "fields": [
        {"name": "start", "type": "string"},
        {"name": "end", "type": "string"}
      ]
    }},
    {"name": "currency", "type": "string"},
    {"name": "media_buy_id", "type": "string"},
    {"name": "status", "type": "string"},
    {"name": "totals", "type": {
      "type": "record",
      "name": "Totals",
      "fields": [
        {"name": "impressions", "type": "long"},
        {"name": "spend", "type": "double"},
        {"name": "clicks", "type": "long"},
        {"name": "ctr", "type": "double"}
      ]
    }},
    {"name": "by_package", "type": {
      "type": "array",
      "items": {
        "type": "record",
        "name": "PackageDelivery",
        "fields": [
          {"name": "package_id", "type": "string"},
          {"name": "impressions", "type": "long"},
          {"name": "spend", "type": "double"},
          {"name": "pacing_index", "type": "double"},
          {"name": "pricing_model", "type": "string"},
          {"name": "rate", "type": "double"},
          {"name": "currency", "type": "string"}
        ]
      }
    }}
  ]
}
```

#### ORC

**For Hive/Spark analytics**

Columnar format optimized for read-heavy analytics on Hadoop-ecosystem tools (Hive, Spark, Presto). Predicate pushdown, built-in indexes, and lightweight compression (snappy, zlib, or zstd) reduce I/O. Supports nested structures via struct and array types.

ORC uses the same logical schema as Parquet. Choose ORC when your data warehouse is Hive-native; choose Parquet for broader tool compatibility.

**File Structure:**
Each file contains one media buy delivery per line (JSONL), row (CSV/Parquet/ORC), or record (Avro). Files may contain:

* Multiple media buy deliveries (one per line/row)
* Multiple reporting periods for the same media buy (separate rows)
* Multiple media buys (each with its own rows)

**Processing Recommendations:**

* Process files in chronological order using file timestamps
* Handle duplicate files gracefully (idempotent processing)
* Validate file integrity using checksums if provided
* Monitor for missing files and alert on gaps

### Security considerations for offline delivery

Offline files sit at rest for `file_retention_days`, so a misconfigured IAM policy leaks historical reporting across tenants. The [general security controls](/docs/building/implementation/security) apply; the offline-specific requirements:

* **Scope access at the IAM layer, not by obscurity.** Buyer read access MUST be scoped to `{bucket}/{prefix}/*` (S3 bucket policy condition, GCS conditional IAM binding on `resource.name.startsWith(...)`, or Azure SAS scoped to the prefix). A bucket-wide read grant is non-compliant even when the seller only writes under one prefix per account.
* **Scope listing as well as reads.** Prefix scoping MUST cover both object-level operations (`s3:GetObject`) and listing (`s3:ListBucket` with an `s3:prefix` condition). A policy that scopes `GetObject` to the prefix but leaves `ListBucket` unscoped lets a buyer enumerate other tenants' prefix names — a cross-tenant isolation failure even without read access to their objects. The same applies to GCS `storage.objects.list` and Azure `list` SAS permissions.
* **Revoke access when the account closes.** When the seller emits an `account.status` transition to `inactive`, `suspended`, or `closed`, the seller MUST stop honoring the associated credentials, and buyers SHOULD treat that status change as the trigger to remove matching IAM trust on their end. A seller IAM role left granted to a decommissioned bucket is a lateral-movement risk.

PII scrubbing requirements (see [above](#pii-scrubbing-for-gdpr-ccpa)) apply to offline files identically — scrub at the collection layer, not at delivery, because the files accumulate at rest.

`setup_instructions` is a seller-provided URL. It is operator-facing documentation, not agent-consumable content. Buyer agents MUST NOT auto-fetch the URL; they SHOULD surface it to a human operator. If an implementation chooses to fetch it (for example, to preview the target before showing it to the operator), apply [webhook URL SSRF validation](/docs/building/implementation/security#webhook-url-validation-ssrf), and the fetched content MUST NOT be passed into an LLM context without indirect-prompt-injection guarding — seller-controlled text in this field can contain instructions to rotate credentials, change billing, or alter downstream agent behavior.

## Data Reconciliation

**The `get_media_buy_delivery` API is the authoritative source of truth for all campaign metrics.** Polling is always available as a baseline. When the seller also supports push-based delivery (webhooks or offline buckets), those methods provide timely data but `get_media_buy_delivery` remains the reconciliation path.

Reconciliation is important for **any reporting delivery method** because:

* **Webhooks**: May be missed due to network failures or circuit breaker drops
* **Offline files**: May be delayed, corrupted, or fail to process
* **Polling**: May miss data during API outages
* **Late-arriving data**: Impressions can arrive 24-48+ hours after initial reporting (all methods)

#### Reconciliation Process

Buyers SHOULD periodically reconcile delivered data against API to ensure accuracy:

**Recommended Reconciliation Schedule:**

* **Hourly delivery**: Reconcile via API daily
* **Daily delivery**: Reconcile via API weekly
* **Monthly delivery**: Reconcile via API at month end + 7 days
* **Campaign close**: Always reconcile after campaign\_end + attribution\_window

**Reconciliation Logic:**

```javascript theme={null}
async function reconcileWebhookData(mediaBuyId, startDate, endDate) {
  // Get authoritative data from API
  const apiData = await adcp.getMediaBuyDelivery({
    media_buy_id: mediaBuyId,
    date_range: { start: startDate, end: endDate }
  });

  // Compare with webhook data in local database
  const webhookData = await db.getWebhookTotals(mediaBuyId, startDate, endDate);

  const discrepancy = {
    impressions: apiData.totals.impressions - webhookData.impressions,
    spend: apiData.totals.spend - webhookData.spend,
    clicks: apiData.totals.clicks - webhookData.clicks
  };

  // Acceptable discrepancy thresholds
  const impressionVariance = Math.abs(discrepancy.impressions) / apiData.totals.impressions;
  const spendVariance = Math.abs(discrepancy.spend) / apiData.totals.spend;

  if (impressionVariance > 0.02 || spendVariance > 0.01) {
    // Significant discrepancy (>2% impressions or >1% spend)
    console.warn(`Reconciliation discrepancy for ${mediaBuyId}:`, discrepancy);

    // Update local database with authoritative API data
    await db.updateCampaignTotals(mediaBuyId, apiData.totals);

    // Alert if discrepancy is unusually large
    if (impressionVariance > 0.10 || spendVariance > 0.05) {
      await alertOps(`Large reconciliation discrepancy detected`, {
        media_buy_id: mediaBuyId,
        webhook_totals: webhookData,
        api_totals: apiData.totals,
        discrepancy
      });
    }
  }

  return {
    status: impressionVariance < 0.02 ? 'reconciled' : 'discrepancy_found',
    api_data: apiData.totals,
    webhook_data: webhookData,
    discrepancy
  };
}
```

**Why Discrepancies Occur:**

1. **Delivery failures**: Webhooks missed, offline files corrupted, API timeouts during polling
2. **Late-arriving data**: Impressions attributed after initial reporting (all delivery methods)
3. **Data corrections**: Publisher adjusts metrics after initial reporting
4. **Processing errors**: Buyer-side failures to process delivered data
5. **Timezone differences**: Period boundaries may differ between delivery and API query

**Source of Truth Rules:**

* **For billing**: Always use `get_media_buy_delivery` API at campaign end + attribution window
* **For real-time decisions**: Use delivered data (webhook/file/poll) for speed, reconcile later
* **For discrepancies**: API data wins, update local records accordingly
* **For audits**: API provides complete historical data, delivered data is ephemeral

**Best Practices:**

* Store webhook `sequence_number` to detect missed notifications
* Run automated reconciliation daily for active campaigns
* Alert on discrepancies >2% for impressions or >1% for spend
* Use API data for all financial reporting and invoicing
* Document reconciliation process for audit compliance

#### Late-Arriving Impressions

Ad serving data often arrives with delays due to attribution windows, offline tracking, and pipeline latency. Publishers declare `expected_delay_minutes` in `reporting_capabilities`:

* **Display/Video**: Typically 4-6 hours
* **Audio**: Typically 8-12 hours
* **CTV**: May be 24+ hours

This represents when **most** data is available, not **all** data.

#### Handling Late Arrivals

When late data arrives for a previously reported period, **resend that period** with `is_adjusted: true`:

```json theme={null}
{
  "notification_type": "adjusted",
  "reporting_period": {
    "start": "2024-02-01T00:00:00Z",
    "end": "2024-02-01T23:59:59Z"
  },
  "media_buy_deliveries": [{
    "media_buy_id": "mb_001",
    "is_adjusted": true,
    "totals": {
      "impressions": 51000,  // Updated total (was 50000)
      "spend": 1785          // Updated spend (was 1750)
    }
  }]
}
```

**Buyer Processing:**

```javascript theme={null}
function processWebhook(webhook) {
  for (const delivery of webhook.media_buy_deliveries) {
    if (delivery.is_adjusted) {
      // Replace entire period with updated totals
      db.replaceCampaignPeriod(
        delivery.media_buy_id,
        webhook.reporting_period,
        delivery.totals
      );
    } else {
      // Normal new period data
      db.insertCampaignPeriod(delivery.media_buy_id, webhook.reporting_period, delivery.totals);
    }
  }
}
```

**When to send adjusted periods:**

* Significant data changes (>2% impression variance or >1% spend variance)
* Final reconciliation at campaign\_end + attribution\_window
* Data quality corrections

With polling-only, buyers detect adjustments through reconciliation by comparing API results over time.

#### Webhook Reliability

Reporting webhooks follow AdCP's standard webhook reliability patterns:

* **At-least-once delivery**: Same notification may be delivered multiple times
* **Best-effort ordering**: Notifications may arrive out of order
* **Timeout and retry**: Limited retry attempts on delivery failure

See [Webhooks](/docs/building/implementation/webhooks) for detailed implementation guidance.

## Optimization Strategies

### Conversion optimization

Set optimization goals on media buy packages to direct delivery toward specific outcomes — target CPC, CPV, ROAS, or CPA. Metric goals (clicks, views) work without event setup. Event goals require configured event sources and conversion data.

See [Conversion Tracking](/docs/media-buy/conversion-tracking/) for the complete setup flow, and [Optimization Goals](/docs/media-buy/conversion-tracking/#optimization-goals) for the `optimization_goals` array reference.

### Budget optimization

* **Reallocation** between high and low performing packages via [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy)
* **Pacing adjustments** — switch between `even`, `asap`, or `front_loaded` delivery
* **Spend efficiency** — compare cost per acquisition across packages and shift budget to the best performers

### Creative optimization

* **Performance analysis** by creative asset via [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery) with by-creative breakdowns
* **A/B testing** — assign multiple creatives with weights via `creative_assignments`
* **Refresh strategies** — swap creatives via [`sync_creatives`](/docs/creative/task-reference/sync_creatives) for library-backed sellers, or inline `packages[].creatives` on [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) for inline-only sellers, to prevent fatigue

### Targeting refinement

* **Geographic optimization** — adjust `targeting_overlay` based on delivery data by region
* **Frequency management** — tune `frequency_cap` (suppress cooldown or max\_impressions/per/window cap) based on delivery patterns

## Performance Feedback Loop

The performance feedback system enables AI-driven optimization by feeding back business outcomes to publishers. See [`provide_performance_feedback`](/docs/media-buy/task-reference/provide_performance_feedback) for detailed API documentation.

### Performance Index Concept

A normalized score indicating relative performance:

* `0.0` = No measurable value or impact
* `1.0` = Baseline/expected performance
* `> 1.0` = Above average (e.g., 1.45 = 45% better)
* `< 1.0` = Below average (e.g., 0.8 = 20% worse)

### Sharing Performance Data

Buyers can voluntarily share performance outcomes using the [`provide_performance_feedback`](/docs/media-buy/task-reference/provide_performance_feedback) task:

```json theme={null}
{
  "media_buy_id": "gam_1234567890",
  "measurement_period": {
    "start": "2024-01-15T00:00:00Z",
    "end": "2024-01-21T23:59:59Z"
  },
  "performance_index": 1.35,
  "metric_type": "conversion_rate"
}
```

### Supported Metrics

* **overall\_performance**: General campaign success
* **conversion\_rate**: Post-click or post-view conversions
* **brand\_lift**: Brand awareness or consideration lift
* **click\_through\_rate**: Engagement with creative
* **completion\_rate**: Video or audio completion rates
* **viewability**: Viewable impression rate
* **brand\_safety**: Brand safety compliance
* **cost\_efficiency**: Cost per desired outcome

### How Publishers Use Performance Data

Publishers can leverage performance indices to:

1. **Optimize Delivery**: Shift impressions to high-performing segments
2. **Adjust Pricing**: Update CPMs based on proven value
3. **Improve Products**: Refine product definitions based on performance patterns
4. **Enhance Algorithms**: Train ML models on actual business outcomes

### Privacy and Data Sharing

* Performance feedback sharing is voluntary and controlled by the buyer
* Aggregate performance patterns may be used to improve overall platform performance
* Individual campaign details remain confidential to the buyer-publisher relationship

### Dimension breakdowns

Delivery data can be broken down across multiple dimensions within each package. Buyers request specific breakdowns via the `reporting_dimensions` parameter on `get_media_buy_delivery`. Each breakdown appears as a `by_*` array within `by_package` items, following the same composition pattern as `by_creative`.

| Dimension       | Breakdown field      | Required fields                                          | Additional fields                    | Capability declaration               |
| --------------- | -------------------- | -------------------------------------------------------- | ------------------------------------ | ------------------------------------ |
| Geography       | `by_geo`             | `geo_level`, `geo_code`, `impressions`, `spend`          | `system`, `country`, `geo_name`      | `supports_geo_breakdown`             |
| Device type     | `by_device_type`     | `device_type`, `impressions`, `spend`                    | —                                    | `supports_device_type_breakdown`     |
| Device platform | `by_device_platform` | `device_platform`, `impressions`, `spend`                | —                                    | `supports_device_platform_breakdown` |
| Audience        | `by_audience`        | `audience_id`, `audience_source`, `impressions`, `spend` | `audience_name`                      | `supports_audience_breakdown`        |
| Placement       | `by_placement`       | `placement_id`, `impressions`, `spend`                   | `publisher_domain`, `placement_name` | `supports_placement_breakdown`       |

Each breakdown entry inherits all fields from `delivery-metrics` (clicks, conversions, and other optional metrics) plus its dimension-specific fields. Every entry requires the fields shown in the required fields column. Check `reporting_capabilities` on the product to discover which dimensions are available; product-level capabilities are authoritative because different products from the same seller may support different breakdowns. `supports_geo_breakdown` is an object that declares available levels and systems; the other capability declarations in this table are boolean flags. Within `supports_geo_breakdown`, `country` and `region` are booleans, while `metro` is keyed by `metro-system` values and native `postal_area` is keyed by ISO 3166-1 alpha-2 country with arrays of country-local `postal-system` values. Geo rows use `system` for `geo_level: "metro"` and `"postal_area"`; native postal rows also include `country`. Deprecated country-fused postal systems remain accepted for compatibility.

Placement identity is publisher-scoped. Placement rows MAY carry `publisher_domain`, the publisher namespace from the product's `placements[]` entry, so buyers can treat `{publisher_domain, placement_id}` as the stable placement identity for multi-publisher products when it is present. Sellers SHOULD emit `publisher_domain` whenever the product placement carries it (always true for `kind: "publisher_ref"`); sellers MAY omit it only for `kind: "seller_inline"` placements in a legacy single-publisher context where the seller agent's own domain is the namespace. When `publisher_domain` is omitted, buyers MAY interpret `placement_id` relative to the seller agent's own publisher domain only in that legacy single-publisher context; otherwise buyers should not infer a cross-publisher placement key. `publisher_domain` is single-valued because each placement belongs to exactly one publisher namespace.

Breakdowns are opt-in — no dimension data is returned unless explicitly requested. Sellers that don't support a requested dimension silently omit it. Each breakdown array has a sibling `by_*_truncated` boolean indicating whether additional rows exist beyond the requested `limit`.

## Targeting Consistency

Reporting aligns with AdCP's [Targeting](/docs/media-buy/advanced-topics/targeting) approach, enabling:

* **Consistent analysis** across campaign lifecycle
* **Granular breakdowns** by targeting parameters
* **Cross-campaign insights** for portfolio optimization

### Target → Measure → Optimize

The power of consistent targeting and reporting creates a virtuous cycle:

1. **Target**: Define your audience using briefs and overlays (e.g., "Mobile users in major metros")
2. **Measure**: Report on the same attributes (Track performance by device type and geography)
3. **Optimize**: Feed performance back to improve delivery (Shift budget to high-performing segments)

## Standard Metrics

All platforms must support these core metrics:

* **impressions**: Number of ad views
* **spend**: Amount spent in currency
* **clicks**: Number of clicks (if applicable)
* **ctr**: Click-through rate (clicks/impressions)

Optional standard metrics:

* **conversions**: Post-click/view conversions
* **viewability**: Percentage of viewable impressions
* **completion\_rate**: Video/audio completion percentage
* **engagement\_rate**: Platform-specific engagement metric

## Platform-Specific Considerations

Different platforms offer varying reporting and optimization capabilities:

### Google Ad Manager

* Comprehensive dimensional reporting, real-time and historical data, advanced viewability metrics

### Kevel

* Real-time reporting API, custom metric support, flexible aggregation options

### Triton Digital

* Audio-specific metrics (completion rates, skip rates), station-level performance data, daypart analysis

## Advanced Analytics

### Cross-Campaign Analysis

* **Portfolio performance** across multiple campaigns
* **Audience overlap** and frequency management
* **Budget allocation** optimization across campaigns

### Predictive Insights

* **Performance forecasting** based on historical data
* **Optimization recommendations** from AI analysis
* **Trend prediction** for proactive adjustments

## Response Times

Optimization operations have predictable timing:

* **Delivery reports**: \~60 seconds (data aggregation)
* **Campaign updates**: Minutes to days (depending on changes)
* **Performance analysis**: \~1 second (cached metrics)

## Best Practices

1. **Report Frequently**: Regular reporting improves optimization opportunities
2. **Track Pacing**: Monitor delivery against targets to avoid under/over-delivery
3. **Analyze Patterns**: Look for performance trends across dimensions
4. **Consider Latency**: Some metrics may have attribution delays
5. **Normalize Metrics**: Use consistent baselines for performance comparison

## Integration with Media Buy Lifecycle

Optimization and reporting is the ongoing phase that runs throughout active campaigns:

* **Connects to Creation**: Use learnings to improve future campaign setup
* **Guides Updates**: Data-driven decisions for campaign modifications
* **Enables Scale**: Proven strategies can be applied to similar campaigns
* **Feeds AI**: Performance data improves automated optimization

## Related Documentation

* **[`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery)** - Retrieve delivery reports
* **[`update_media_buy`](/docs/media-buy/task-reference/update_media_buy)** - Modify campaigns based on performance
* **[Media Buy Lifecycle](/docs/media-buy/media-buys)** - Complete campaign management workflow
* **[Targeting](/docs/media-buy/advanced-topics/targeting)** - Brief-based targeting and overlays
