{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/3.1.0-rc.4/core/delivery-metrics.json",
  "title": "Delivery Metrics",
  "description": "Standard delivery metrics that can be reported at media buy, package, or creative level",
  "type": "object",
  "properties": {
    "impressions": {
      "type": "number",
      "description": "Impressions delivered",
      "minimum": 0
    },
    "spend": {
      "type": "number",
      "description": "Amount spent",
      "minimum": 0
    },
    "clicks": {
      "type": "number",
      "description": "Total clicks",
      "minimum": 0
    },
    "ctr": {
      "type": "number",
      "description": "Click-through rate (clicks/impressions)",
      "minimum": 0,
      "maximum": 1
    },
    "views": {
      "type": "number",
      "description": "Content engagements counted toward the billable view threshold. For video this is a platform-defined view event (e.g., 30 seconds or video midpoint); for audio/podcast it is a stream start; for other formats it follows the pricing model's view definition. When the package uses CPV pricing, spend = views × rate.",
      "minimum": 0
    },
    "completed_views": {
      "type": "number",
      "description": "Video/audio completions. When the package has a completed_views optimization goal with view_duration_seconds, completions are counted at that threshold rather than 100% completion.",
      "minimum": 0
    },
    "completion_rate": {
      "type": "number",
      "description": "Completion rate (completed_views/impressions)",
      "minimum": 0,
      "maximum": 1
    },
    "conversions": {
      "type": "number",
      "description": "Total conversions attributed to this delivery. When by_event_type is present, this equals the sum of all by_event_type[].count entries.",
      "minimum": 0
    },
    "conversion_value": {
      "type": "number",
      "description": "Total monetary value of attributed conversions (in the reporting currency)",
      "minimum": 0
    },
    "roas": {
      "type": "number",
      "description": "Return on ad spend (conversion_value / spend)",
      "minimum": 0
    },
    "cost_per_acquisition": {
      "type": "number",
      "description": "Cost per conversion (spend / conversions)",
      "minimum": 0
    },
    "new_to_brand_rate": {
      "type": "number",
      "description": "Fraction of `conversions` (transactions) from first-time brand buyers, 0 = none, 1 = all. For retail-media unit-volume tracking of first-time buyers, see `new_to_brand_units` (count, not rate).",
      "minimum": 0,
      "maximum": 1
    },
    "leads": {
      "type": "number",
      "description": "Leads generated (convenience alias for by_event_type where event_type='lead')",
      "minimum": 0
    },
    "incremental_sales_lift": {
      "type": "number",
      "description": "Incremental sales lift attributed to the campaign — sales above the control/holdout baseline. Reported as a fraction (0.15 = 15% lift) or as an absolute value depending on seller convention. The seller's `attribution_methodology` qualifier (typically `deterministic_purchase` or `modeled`) and `attribution_window` qualifier on the matching `committed_metrics` entry disambiguate the methodology and window.",
      "minimum": 0
    },
    "brand_lift": {
      "type": "number",
      "description": "Brand lift — measured change in a brand metric (awareness, consideration, favorability, purchase intent, or ad recall) attributed to the campaign. Typically panel-based or survey-based. Reported as a fraction (0.05 = 5% lift). **Multidimensional in production** — Kantar, Upwave, Cint, DV all report each dimension separately with its own sample size and confidence interval. The dimension flows through `qualifier.lift_dimension` on `committed_metrics` / `metric_aggregates` (`awareness` | `consideration` | `favorability` | `purchase_intent` | `ad_recall`); rows under different dimensions are different surveyed outcomes and must not be combined. Use `attribution_methodology: 'panel_based'` qualifier when the underlying methodology is a panel.",
      "minimum": 0
    },
    "foot_traffic": {
      "type": "number",
      "description": "Store visits attributed to ad exposure. Count of incremental visits over baseline. Typically uses location-data panel methodology (`attribution_methodology: 'panel_based'`) or deterministic loyalty-card match (`attribution_methodology: 'deterministic_purchase'`).",
      "minimum": 0
    },
    "conversion_lift": {
      "type": "number",
      "description": "Incremental conversions attributed to the campaign — conversions above the control/holdout baseline. Reported as a fraction (0.10 = 10% lift) or as an absolute count depending on seller convention. Distinct from `conversions` (raw count of attributed conversions); conversion_lift requires a control group and an incrementality methodology.",
      "minimum": 0
    },
    "brand_search_lift": {
      "type": "number",
      "description": "Lift in brand search query volume attributed to the campaign — measured via search-data partnerships (Google, Microsoft) or survey methodology. Reported as a fraction (0.20 = 20% lift in branded search).",
      "minimum": 0
    },
    "plays": {
      "type": "number",
      "description": "Number of times the ad creative was displayed on a DOOH screen or played in a loop. Raw play count before any impression multiplier is applied. Mirrors `forecastable-metric.json`'s `plays` token for forecast↔delivery reconciliation. Distinct from `dooh_metrics.loop_plays` (per-screen rotation count) and from `impressions` (multiplied audience figure). Used for DOOH and broadcast inventory where buyers reconcile against forecast `plays`.",
      "minimum": 0
    },
    "by_event_type": {
      "type": "array",
      "description": "Conversion metrics broken down by event type. Spend-derived metrics (ROAS, CPA) are only available at the package/totals level since spend cannot be attributed to individual event types.",
      "items": {
        "type": "object",
        "properties": {
          "event_type": {
            "$ref": "/schemas/3.1.0-rc.4/enums/event-type.json",
            "description": "The event type"
          },
          "event_source_id": {
            "type": "string",
            "description": "Event source that produced these conversions (for disambiguation when multiple event sources are configured)"
          },
          "count": {
            "type": "number",
            "description": "Number of events of this type",
            "minimum": 0
          },
          "value": {
            "type": "number",
            "description": "Total monetary value of events of this type",
            "minimum": 0
          }
        },
        "required": [
          "event_type",
          "count"
        ],
        "additionalProperties": true
      }
    },
    "grps": {
      "type": "number",
      "description": "Gross Rating Points delivered (for CPP)",
      "minimum": 0
    },
    "reach": {
      "type": "number",
      "description": "Unique reach in the units specified by reach_unit. When reach_unit is omitted, units are unspecified — do not compare reach values across packages or media buys without a common reach_unit. The measurement window for this value is declared in `reach_window`; when `reach_window` is omitted, the window is unspecified and buyers MUST NOT sum reach across reports (the value MAY be a daily snapshot, a cumulative total, or something else).",
      "minimum": 0
    },
    "reach_unit": {
      "allOf": [{ "$ref": "/schemas/3.1.0-rc.4/enums/reach-unit.json" }],
      "description": "Unit of measurement for the reach field. Aligns with the reach_unit declared on optimization goals and delivery forecasts. Required when reach is present to enable cross-platform comparison."
    },
    "reach_window": {
      "type": "object",
      "description": "Measurement window for the reported `reach` and `frequency` values in this row. Declares whether the values are a per-period snapshot, a trailing rolling window, or cumulative-to-date — without this declaration, buyers summing `reach` across rows (e.g., daily delivery reports) can silently double-count audiences. Sellers SHOULD populate this whenever `reach` is present.",
      "properties": {
        "kind": {
          "type": "string",
          "enum": ["cumulative", "period", "rolling"],
          "description": "Window semantics. `cumulative` — uniques since campaign start; the value is the total unique count to date and MUST NOT be summed across rows (each later row supersedes the earlier value). `period` — uniques within a single non-overlapping reporting period (e.g., a daily snapshot for a specific calendar day). Adjacent `period` rows do not share audiences by construction, but the same person MAY appear across multiple periods, so MUST NOT be summed across rows to compute campaign reach. `rolling` — uniques within a trailing window ending at the row's reporting timestamp (e.g., trailing-7-day reach). Adjacent rolling rows overlap and MUST NOT be summed; each row's value stands alone."
        },
        "period": {
          "allOf": [{ "$ref": "/schemas/3.1.0-rc.4/core/duration.json" }],
          "description": "Duration of the measurement window. REQUIRED when `kind` is `period` or `rolling` — declares the snapshot length (e.g., `{\"interval\": 1, \"unit\": \"days\"}` for a daily snapshot) or the trailing-window length (e.g., `{\"interval\": 7, \"unit\": \"days\"}` for trailing-7-day rolling reach). When `kind` is `cumulative`, this field is implicit (campaign-to-date) and SHOULD be omitted."
        }
      },
      "required": ["kind"],
      "allOf": [
        {
          "if": {
            "properties": { "kind": { "enum": ["period", "rolling"] } },
            "required": ["kind"]
          },
          "then": {
            "required": ["period"]
          }
        }
      ],
      "additionalProperties": true
    },
    "frequency": {
      "type": "number",
      "description": "Average frequency per reach unit, measured over the window declared in `reach_window`. When `reach_unit` is 'households', this is average exposures per household; when 'accounts', per logged-in account; etc. When `reach_window` is omitted, the window is unspecified — buyers MUST NOT compare or average frequency values across rows.",
      "minimum": 0
    },
    "quartile_data": {
      "type": "object",
      "description": "Audio/video quartile completion data",
      "properties": {
        "q1_views": {
          "type": "number",
          "description": "25% completion views",
          "minimum": 0
        },
        "q2_views": {
          "type": "number",
          "description": "50% completion views",
          "minimum": 0
        },
        "q3_views": {
          "type": "number",
          "description": "75% completion views",
          "minimum": 0
        },
        "q4_views": {
          "type": "number",
          "description": "100% completion views",
          "minimum": 0
        }
      }
    },
    "dooh_metrics": {
      "type": "object",
      "description": "DOOH-specific metrics (only included for DOOH campaigns)",
      "properties": {
        "loop_plays": {
          "type": "integer",
          "description": "Number of times ad played in rotation",
          "minimum": 0
        },
        "screens_used": {
          "type": "integer",
          "description": "Number of unique screens displaying the ad",
          "minimum": 0
        },
        "screen_time_seconds": {
          "type": "integer",
          "description": "Total display time in seconds",
          "minimum": 0
        },
        "sov_achieved": {
          "type": "number",
          "description": "Actual share of voice delivered (0.0 to 1.0)",
          "minimum": 0,
          "maximum": 1
        },
        "calculation_notes": {
          "type": "string",
          "description": "Per-row supplementary methodology notes for DOOH impression calculation (e.g., 'rotation-based; 6-second slot weighted by 70% audience overlap'). Free-form prose for context that doesn't fit the structured measurement-vendor surface. Canonical methodology declarations belong on the measurement vendor's `get_adcp_capabilities.measurement.metrics[]` block where they're discoverable once and inherited across delivery rows; this field is for row-specific context (a particular daypart's calculation, a venue-mix exception) rather than the seller's general methodology."
        },
        "venue_breakdown": {
          "type": "array",
          "description": "Per-venue performance breakdown",
          "items": {
            "type": "object",
            "properties": {
              "venue_id": {
                "type": "string",
                "description": "Venue identifier"
              },
              "venue_name": {
                "type": "string",
                "description": "Human-readable venue name"
              },
              "venue_type": {
                "type": "string",
                "description": "Venue type (e.g., 'airport', 'transit', 'retail', 'billboard')"
              },
              "impressions": {
                "type": "integer",
                "description": "Impressions delivered at this venue",
                "minimum": 0
              },
              "loop_plays": {
                "type": "integer",
                "description": "Loop plays at this venue",
                "minimum": 0
              },
              "screens_used": {
                "type": "integer",
                "description": "Number of screens used at this venue",
                "minimum": 0
              }
            },
            "required": [
              "venue_id",
              "impressions"
            ],
            "additionalProperties": true
          }
        }
      },
      "additionalProperties": true
    },
    "viewability": {
      "type": "object",
      "description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable.",
      "properties": {
        "vendor": {
          "$ref": "/schemas/3.1.0-rc.4/core/brand-ref.json",
          "description": "Vendor that produced these viewability values. Optional but RECOMMENDED so the row is self-describing — buyer agents reading delivery in isolation can attribute the numbers without joining back to `package.committed_metrics` or `package.performance_standards`. The vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; the metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Same shape as `vendor_metric_value.vendor` for symmetry across vendor-attested surfaces."
        },
        "measurable_impressions": {
          "type": "number",
          "description": "Impressions where viewability could be measured. Excludes environments without measurement capability (e.g., non-Intersection Observer browsers, certain app environments). Coverage denominator for `viewable_rate` AND `viewed_seconds` — both metrics are computed over the same measurable population.",
          "minimum": 0
        },
        "viewable_impressions": {
          "type": "number",
          "description": "Impressions that met the viewability threshold defined by the measurement standard.",
          "minimum": 0
        },
        "viewable_rate": {
          "type": "number",
          "description": "Viewable impression rate (viewable_impressions / measurable_impressions). Range 0.0 to 1.0.",
          "minimum": 0,
          "maximum": 1
        },
        "viewed_seconds": {
          "type": "number",
          "description": "Average in-view duration per measurable impression, in seconds. Reporting-side counterpart to the `viewed_seconds` optimization metric in `optimization-goal.json`. Computed over `measurable_impressions`, not total impressions — the same denominator as `viewable_rate`. The viewability `standard` governs the threshold (e.g., MRC's 50% pixels for 1s display / 2s video) that defines when an impression is in view and therefore when the clock is running. Sellers reporting against a `viewed_seconds` optimization goal MUST populate this field.",
          "minimum": 0
        },
        "standard": {
          "$ref": "/schemas/3.1.0-rc.4/enums/viewability-standard.json",
          "description": "Viewability measurement standard applied to these metrics. Governs the in-view threshold for both `viewable_rate` and `viewed_seconds`."
        }
      },
      "additionalProperties": true
    },
    "engagements": {
      "type": "number",
      "description": "Total engagements — direct interactions with the ad beyond viewing. Includes social reactions/comments/shares, story/unit opens, interactive overlay taps on CTV, companion banner interactions on audio. Platform-specific; corresponds to the 'engagements' optimization metric. Maps to DBCFM KPI_INTERACTIONS (Interaktionen) in the Reporting/Performance block.",
      "minimum": 0
    },
    "follows": {
      "type": "number",
      "description": "New followers, page likes, artist/podcast/channel subscribes attributed to this delivery.",
      "minimum": 0
    },
    "saves": {
      "type": "number",
      "description": "Saves, bookmarks, playlist adds, pins attributed to this delivery.",
      "minimum": 0
    },
    "profile_visits": {
      "type": "number",
      "description": "Visits to the brand's in-platform page (profile, artist page, channel, or storefront) attributed to this delivery. Does not include external website clicks.",
      "minimum": 0
    },
    "engagement_rate": {
      "type": "number",
      "description": "Platform-specific engagement rate (0.0 to 1.0). Typically engagements/impressions, but definition varies by platform.",
      "minimum": 0,
      "maximum": 1
    },
    "cost_per_click": {
      "type": "number",
      "description": "Cost per click (spend / clicks)",
      "minimum": 0
    },
    "cost_per_completed_view": {
      "type": "number",
      "description": "Cost per completed view (spend / completed_views). Primary CPCV pricing scalar for video/audio inventory; the package's `pricing_model` is `cpcv` when this field is the billing basis.",
      "minimum": 0
    },
    "cpm": {
      "type": "number",
      "description": "Cost per thousand impressions, computed as (spend / impressions) × 1000. Universal pricing scalar across CTV, display, mobile/web video, native, audio, and DOOH inventory; the package's `pricing_model` is `cpm` when this field is the billing basis. Field name aligns with the canonical `cpm` token in `enums/pricing-model.json` and `pricing-options/cpm-option.json` so buyers cross-walk pricing model → reported scalar without a translation table.",
      "minimum": 0
    },
    "downloads": {
      "type": "number",
      "description": "Audio/podcast downloads (IAB Podcast Measurement Technical Guidelines 2.x methodology). Distinct from `views` — for podcast inventory this is the count of podcast episode downloads; for streaming audio it is the count of stream starts that meet the platform's download threshold. Prefer this over `views` for audio inventory.",
      "minimum": 0
    },
    "units_sold": {
      "type": "number",
      "description": "Items sold attributed to this delivery. Retail-media scalar distinct from `conversions` — a single conversion (transaction) may carry multiple `units_sold`. Used by retail media platforms where the buyer optimizes against unit movement, not transaction count. Attribution lookback windows are platform-specific (commonly 7/14/30 days, view-through and click-through variants); sellers SHOULD declare the window via `reporting_capabilities.measurement_windows` or `measurement_terms` rather than encoding it in this scalar.",
      "minimum": 0
    },
    "new_to_brand_units": {
      "type": "number",
      "description": "Units sold to first-time brand buyers (count, not rate). Retail-media scalar — the unit-volume parallel to the conversion-fraction `new_to_brand_rate`. Used by retail media platforms where new-customer acquisition unit volume is a primary KPI. Same attribution-window note as `units_sold` applies.",
      "minimum": 0
    },
    "by_action_source": {
      "type": "array",
      "description": "Conversion metrics broken down by action source (website, app, in_store, etc.). Useful for omnichannel sellers where conversions occur across digital and physical channels.",
      "items": {
        "type": "object",
        "properties": {
          "action_source": {
            "$ref": "/schemas/3.1.0-rc.4/enums/action-source.json",
            "description": "Where the conversion occurred"
          },
          "event_source_id": {
            "type": "string",
            "description": "Event source that produced these conversions (for disambiguation when multiple event sources are configured)"
          },
          "count": {
            "type": "number",
            "description": "Number of conversions from this action source",
            "minimum": 0
          },
          "value": {
            "type": "number",
            "description": "Total monetary value of conversions from this action source",
            "minimum": 0
          }
        },
        "required": [
          "action_source",
          "count"
        ],
        "additionalProperties": true
      }
    },
    "vendor_metric_values": {
      "type": "array",
      "description": "Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared. Each entry carries the vendor (BrandRef), the metric identifier within the vendor's vocabulary, the value, optional unit, and `measurable_impressions` as the coverage denominator — vendor measurement is rarely 100% of delivered impressions, since vendors only score impressions where their SDK fires or their panel matches. When a declared vendor metric is omitted from this array, buyers infer no measurement happened (no integration). One row per `(vendor.domain, vendor.brand_id, metric_id)` per reporting period — sellers MUST de-duplicate before emission and MUST NOT emit the same vendor metric twice; buyers MAY treat duplicate rows as a seller-side conformance bug. The structured `vendor_metric_values` array is the recommended path for vendor metrics; `additionalProperties: true` on this parent object is preserved so existing free-form vendor emissions remain conformant during migration.",
      "items": {
        "$ref": "/schemas/3.1.0-rc.4/core/vendor-metric-value.json"
      }
    }
  },
  "dependencies": {
    "reach": ["reach_unit"]
  },
  "additionalProperties": true
}
