From fbc44354cb5b8f309481289817852ea74ac8ac94 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 4 Oct 2025 07:10:11 -0400 Subject: [PATCH 01/16] feat: add support for non-impression pricing models (CPM, CPCV, CPP, CPA, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds comprehensive support for multiple pricing models beyond CPM, enabling TV/audio (CPP/GRP), video (CPCV/CPV), and performance (CPA/CPL/CPC) campaigns. Publishers declare available pricing options per product: - Multiple pricing models per product (CPM, CPCV, CPP, etc.) - Multi-currency support (USD, EUR, GBP, etc.) - Fixed or auction-based pricing per option Buyers select from available options when creating media buys: - Currency set at campaign level (applies to all packages) - Pricing model selected per package - Simplified flat structure with pacing support - **CPM** - Cost per 1,000 impressions (traditional display) - **CPCV** - Cost per completed view (100% video/audio completion) - **CPV** - Cost per view at threshold (e.g., 50% completion) - **CPC** - Cost per click - **CPA** - Cost per action/acquisition - **CPL** - Cost per lead - **CPP** - Cost per point (GRP-based for TV/audio) - **Flat Rate** - Fixed cost regardless of delivery **New schemas:** - `pricing-model.json` - Enum of supported pricing models - `pricing-option.json` - Publisher pricing option structure **Updated schemas:** - `product.json` - Added `pricing_options` array (deprecated `cpm`/`is_fixed_price`) - `create-media-buy-request.json` - Flattened structure with currency at campaign level - `get-media-buy-delivery-response.json` - Enhanced metrics for all pricing models - `budget.json` - Simplified (always monetary amount) - New comprehensive pricing models guide (350+ lines) - Updated media products documentation with examples - Added 15+ glossary terms (CPA, CPCV, CPP, GRP, etc.) - Package structure simplified: currency moved to campaign level - Removed delivery_goals (can be added in future version if needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 424 ++++++++++++++++++ .../product-discovery/media-products.md | 165 +++++-- docs/reference/glossary.md | 73 ++- static/schemas/v1/core/budget.json | 4 +- static/schemas/v1/core/pricing-option.json | 114 +++++ static/schemas/v1/core/product.json | 43 +- static/schemas/v1/enums/pricing-model.json | 27 ++ static/schemas/v1/index.json | 10 +- .../media-buy/create-media-buy-request.json | 2 +- .../get-media-buy-delivery-response.json | 111 ++++- .../schemas/v1/media-buy/package-request.json | 40 +- tests/schema-validation.test.js | 10 +- 12 files changed, 938 insertions(+), 85 deletions(-) create mode 100644 docs/media-buy/advanced-topics/pricing-models.md create mode 100644 static/schemas/v1/core/pricing-option.json create mode 100644 static/schemas/v1/enums/pricing-model.json diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md new file mode 100644 index 0000000000..fa63f8e531 --- /dev/null +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -0,0 +1,424 @@ +--- +title: Pricing Models +description: Comprehensive guide to AdCP's flexible pricing models including CPM, CPCV, CPP, CPA, and more +keywords: [pricing models, CPM, CPCV, CPP, CPA, CPL, CPC, GRP, video pricing, performance pricing] +--- + +# Pricing Models + +AdCP supports multiple pricing models to accommodate different advertising channels and business objectives. Publishers declare which pricing models they support, and buyers select from available options. + +## Publisher-Declared, Buyer-Selected Model + +### How It Works + +1. **Publishers declare pricing options** in their products via `pricing_options` array +2. **Buyers discover available options** through `get_products` +3. **Buyers select a pricing model** when creating a media buy via `pricing_selection` +4. **Delivery is measured** according to the selected pricing model + +### Key Benefits + +- **Flexibility**: Publishers can offer multiple pricing models for the same inventory +- **Currency Support**: Publishers specify supported currencies; buyers must match +- **Market Standards**: Each channel (TV, video, display, performance) can use its natural pricing unit +- **Clear Expectations**: Both parties agree on pricing before campaign launch + +## Supported Pricing Models + +### CPM (Cost Per Mille) +**Cost per 1,000 impressions** - Traditional display advertising pricing. + +**Use Cases**: Display, native, banner advertising + +**Example**: +```json +{ + "pricing_model": "cpm", + "rate": 12.50, + "currency": "USD", + "is_fixed": true, + "min_spend": 5000 +} +``` + +**Billing**: Charged per 1,000 ad impressions served + +--- + +### CPCV (Cost Per Completed View) +**Cost per 100% video/audio completion** - Payment only for fully completed views. + +**Use Cases**: Video campaigns, audio ads, pre-roll video + +**Example**: +```json +{ + "pricing_model": "cpcv", + "rate": 0.15, + "currency": "USD", + "is_fixed": true, + "parameters": { + "view_threshold": 1.0 + } +} +``` + +**Billing**: Charged only when viewer completes 100% of the video/audio ad + +--- + +### CPV (Cost Per View) +**Cost per view at threshold** - Payment when viewer reaches publisher-defined threshold. + +**Use Cases**: Video campaigns with shorter completion requirements + +**Example**: +```json +{ + "pricing_model": "cpv", + "rate": 0.08, + "currency": "USD", + "is_fixed": true, + "parameters": { + "view_threshold": 0.5 + } +} +``` + +**Billing**: Charged when viewer reaches threshold (e.g., 50% completion, 30 seconds) + +**Parameters**: +- `view_threshold`: Decimal from 0.0 to 1.0 (e.g., 0.5 = 50% completion) + +--- + +### CPP (Cost Per Point) +**Cost per Gross Rating Point** - Traditional TV/radio buying metric. + +**Use Cases**: Connected TV, linear TV, radio, audio streaming + +**Example**: +```json +{ + "pricing_model": "cpp", + "rate": 250.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "demographic": "A18-49", + "min_points": 50 + }, + "min_spend": 12500 +} +``` + +**Billing**: Charged per rating point delivered to target demographic + +**Parameters**: +- `demographic`: Target demographic (e.g., "A18-49", "W25-54", "M35+") +- `min_points`: Minimum GRP commitment required + +**Metrics Reported**: +- `grps`: Total Gross Rating Points delivered +- `reach`: Unique individuals reached +- `frequency`: Average frequency per individual + +--- + +### CPC (Cost Per Click) +**Cost per click** - Performance-based pricing for engagement. + +**Use Cases**: Direct response campaigns, search ads, social advertising + +**Example**: +```json +{ + "pricing_model": "cpc", + "rate": 1.50, + "currency": "USD", + "is_fixed": false, + "price_guidance": { + "floor": 0.50, + "p50": 1.20, + "p75": 2.00 + } +} +``` + +**Billing**: Charged only when user clicks the ad + +--- + +### CPA (Cost Per Action/Acquisition) +**Cost per conversion** - Payment based on completed actions or acquisitions. + +**Use Cases**: E-commerce, lead generation, app installs, performance marketing + +**Example**: +```json +{ + "pricing_model": "cpa", + "rate": 25.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "action_type": "purchase", + "attribution_window_days": 30 + } +} +``` + +**Billing**: Charged per conversion/acquisition event + +**Parameters**: +- `action_type`: Type of action (e.g., "purchase", "sign_up", "download", "add_to_cart") +- `attribution_window_days`: Attribution window (e.g., 7, 14, 30 days) + +--- + +### CPL (Cost Per Lead) +**Cost per lead generated** - Payment for qualified leads. + +**Use Cases**: B2B marketing, form fills, newsletter signups, quote requests + +**Example**: +```json +{ + "pricing_model": "cpl", + "rate": 50.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "attribution_window_days": 7 + } +} +``` + +**Billing**: Charged per qualified lead generated + +--- + +### Flat Rate +**Fixed cost** - Single payment regardless of delivery volume. + +**Use Cases**: Sponsorships, takeovers, exclusive placements, branded content + +**Example**: +```json +{ + "pricing_model": "flat_rate", + "rate": 50000.00, + "currency": "USD", + "is_fixed": true +} +``` + +**Billing**: Fixed cost for the entire campaign period + +--- + +## Multi-Currency Support + +Publishers can offer the same product in multiple currencies: + +```json +{ + "product_id": "premium_video", + "pricing_options": [ + { + "pricing_model": "cpm", + "rate": 45.00, + "currency": "USD", + "is_fixed": true + }, + { + "pricing_model": "cpm", + "rate": 40.00, + "currency": "EUR", + "is_fixed": true + }, + { + "pricing_model": "cpm", + "rate": 35.00, + "currency": "GBP", + "is_fixed": true + } + ] +} +``` + +**Buyer Responsibility**: Buyers must select a currency that the publisher supports. + +## Fixed vs. Auction Pricing + +### Fixed Pricing (`is_fixed: true`) +- Publisher sets a fixed rate +- Rate is guaranteed and predictable +- Common for guaranteed inventory +- Requires `rate` field + +### Auction Pricing (`is_fixed: false`) +- Final price determined through auction +- Publisher provides `price_guidance` with floor and percentiles +- Common for non-guaranteed inventory +- Buyer submits `bid_price` in media buy request + +**Auction Example**: +```json +{ + "pricing_model": "cpcv", + "currency": "USD", + "is_fixed": false, + "price_guidance": { + "floor": 0.08, + "p25": 0.10, + "p50": 0.12, + "p75": 0.15, + "p90": 0.18 + } +} +``` + +## Buyer Selection Process + +Currency is set at the **media buy level**, packages specify their pricing model and budget allocation: + +```json +{ + "buyer_ref": "campaign_001", + "budget": { + "total": 100000, + "currency": "USD" + }, + "start_time": "2025-01-01T00:00:00Z", + "end_time": "2025-01-31T23:59:59Z", + "promoted_offering": "Q1 Brand Campaign", + "packages": [{ + "buyer_ref": "pkg_ctv", + "products": ["premium_ctv"], + "format_ids": ["video_15s", "video_30s"], + "budget": 50000, + "pacing": "even", + "pricing_model": "cpcv", + "bid_price": 0.16 + }] +} +``` + +**How it works:** +1. Media buy sets overall `budget.currency` (e.g., "USD") - applies to all packages +2. Each package selects a `pricing_model` (e.g., "cpcv") +3. Currency + pricing_model identify which pricing option from the product applies +4. Package sets its budget allocation as a number in the media buy's currency +5. Package can specify `pacing` strategy (even, frontload, etc.) +6. If auction-based, package includes `bid_price` + +## Reporting Metrics by Pricing Model + +Different pricing models report different primary metrics: + +| Pricing Model | Primary Metric | Secondary Metrics | +|---------------|----------------|-------------------| +| CPM | impressions | clicks, ctr, spend | +| CPCV | completed_views | impressions, completion_rate, spend | +| CPV | views | impressions, quartile_data, spend | +| CPP | grps | reach, frequency, spend | +| CPC | clicks | impressions, ctr, spend | +| CPA | conversions | impressions, clicks, spend | +| CPL | leads | impressions, clicks, spend | +| Flat Rate | N/A | impressions, reach, frequency | + +## Example: Multi-Model CTV Product + +A publisher offering Connected TV inventory with multiple pricing options: + +```json +{ + "product_id": "ctv_premium_sports", + "name": "Premium Sports CTV", + "description": "High-engagement sports content on CTV devices", + "format_ids": ["video_15s", "video_30s"], + "delivery_type": "guaranteed", + "pricing_options": [ + { + "pricing_model": "cpm", + "rate": 55.00, + "currency": "USD", + "is_fixed": true, + "min_spend": 15000 + }, + { + "pricing_model": "cpcv", + "rate": 0.22, + "currency": "USD", + "is_fixed": true, + "min_spend": 15000 + }, + { + "pricing_model": "cpp", + "rate": 300.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "demographic": "M18-49", + "min_points": 50 + }, + "min_spend": 15000 + } + ] +} +``` + +A buyer could choose CPP pricing if they're planning TV buys, CPCV if optimizing for engagement, or CPM for reach-based campaigns. + +## Best Practices + +### For Publishers + +1. **Offer relevant pricing models** - Match pricing to your inventory type and buyer expectations +2. **Set appropriate minimums** - Use `min_spend` to ensure campaign viability +3. **Provide price guidance** - For auction pricing, give realistic floor and percentile data +4. **Consider multi-currency** - Support currencies of your target markets +5. **Document parameters** - Clearly explain thresholds, demographics, and action types + +### For Buyers + +1. **Select appropriate model** - Choose pricing that aligns with campaign objectives +2. **Match currency** - Ensure you select a currency the publisher supports +3. **Set realistic budgets** - Account for minimum spend requirements +4. **Align goals with pricing** - Set delivery goals that match your pricing model +5. **Monitor relevant metrics** - Focus on the metrics that matter for your pricing model + +## Migration from CPM-Only + +For backward compatibility, products can still use the deprecated `is_fixed_price` and `cpm` fields. However, new implementations should use `pricing_options`. + +**Old Format** (deprecated): +```json +{ + "product_id": "display_standard", + "is_fixed_price": true, + "cpm": 12.50 +} +``` + +**New Format**: +```json +{ + "product_id": "display_standard", + "pricing_options": [{ + "pricing_model": "cpm", + "rate": 12.50, + "currency": "USD", + "is_fixed": true + }] +} +``` + +## Related Documentation + +- [Media Products](../product-discovery/media-products.md) - Product model reference +- [Creating Media Buys](../task-reference/create_media_buy.md) - How to select pricing when buying +- [Delivery Reporting](../task-reference/get_media_buy_delivery.md) - Understanding metrics by pricing model +- [Glossary](../../reference/glossary.md) - Pricing and metric definitions diff --git a/docs/media-buy/product-discovery/media-products.md b/docs/media-buy/product-discovery/media-products.md index e7cff2090e..cd13169f2c 100644 --- a/docs/media-buy/product-discovery/media-products.md +++ b/docs/media-buy/product-discovery/media-products.md @@ -13,39 +13,66 @@ A **Product** is the core sellable unit in AdCP. This document details the Produ - `description` (string, required) - `formats` (list[Format], required): See [Creative Formats](../../creative/formats.md). - `delivery_type` (string, required): Either `"guaranteed"` or `"non_guaranteed"`. -- `is_fixed_price` (bool, required): `true` if the price is fixed, `false` if it is auction-based. -- `cpm` (float, optional): The fixed Cost Per Mille. **Required** if `is_fixed_price` is `true`. -- `price_guidance` (PriceGuidance, optional): Pricing guidance for auction-based products. **Required** if `is_fixed_price` is `false`. -- `min_spend` (float, optional): Minimum budget requirement in USD. +- `pricing_options` (list[PricingOption], required): Array of available pricing models for this product. See [Pricing Models](#pricing-models). - `measurement` (Measurement, optional): Included measurement capabilities. Common for retail media products. - `creative_policy` (CreativePolicy, optional): Creative requirements and restrictions. - `is_custom` (bool, optional): `true` if the product was generated for a specific brief. - `expires_at` (datetime, optional): If `is_custom`, the time the product is no longer valid. +### Deprecated Fields + +- `is_fixed_price` (bool): **DEPRECATED in v1.7.0** - Use `pricing_options` instead. +- `cpm` (float): **DEPRECATED in v1.7.0** - Use `pricing_options` instead. +- `min_spend` (float): **DEPRECATED in v1.7.0** - Use `pricing_options[].min_spend` instead. + ### Pricing Models -AdCP supports two pricing models, determined by the `is_fixed_price` flag. +Publishers declare which pricing models they support for each product. Buyers select from the available options when creating a media buy. This approach supports: + +- **Multiple pricing models per product** - Publishers can offer the same inventory via different pricing structures +- **Multi-currency support** - Publishers declare supported currencies; buyers must use a supported currency +- **Flexible pricing** - Support for CPM, CPCV, CPP (GRP-based), CPA, and more + +#### Supported Pricing Models -#### Guaranteed, Fixed-Price Products -These products represent reserved inventory with a predictable price and delivery. -- `delivery_type`: `"guaranteed"` -- `is_fixed_price`: `true` -- `cpm`: A fixed float value (e.g., `45.00`). +- **CPM** (Cost Per Mille) - Cost per 1,000 impressions (traditional display) +- **CPC** (Cost Per Click) - Cost per click on the ad +- **CPCV** (Cost Per Completed View) - Cost per 100% video/audio completion +- **CPV** (Cost Per View) - Cost per view at publisher-defined threshold +- **CPA** (Cost Per Action) - Cost per conversion/acquisition +- **CPL** (Cost Per Lead) - Cost per lead generated +- **CPP** (Cost Per Point) - Cost per Gross Rating Point (TV/audio) +- **Flat Rate** - Fixed cost regardless of delivery volume -#### Non-Guaranteed, Variable-Price Products -These products represent inventory available in an auction. The final price is not fixed. -- `delivery_type`: `"non_guaranteed"` -- `is_fixed_price`: `false` -- `price_guidance`: A `PriceGuidance` object that helps the buyer make an informed bid. +#### PricingOption Structure + +Each pricing option includes: +```json +{ + "pricing_model": "cpcv", + "rate": 0.15, + "currency": "USD", + "is_fixed": true, + "parameters": { + "view_threshold": 1.0 + }, + "min_spend": 5000 +} +``` -**Example `PriceGuidance`:** +For auction-based pricing (`is_fixed: false`), include `price_guidance`: ```json { - "floor": 10.00, - "p25": 12.50, - "p50": 15.00, - "p75": 18.00, - "p90": 22.00 + "pricing_model": "cpm", + "currency": "USD", + "is_fixed": false, + "price_guidance": { + "floor": 10.00, + "p25": 12.50, + "p50": 15.00, + "p75": 18.00, + "p90": 22.00 + } } ``` @@ -80,39 +107,80 @@ A server can offer a general catalog, but it can also return: ## Product Examples -### Standard Product +### Standard CTV Product (Multiple Pricing Options) ```json { "product_id": "connected_tv_prime", "name": "Connected TV - Prime Time", "description": "Premium CTV inventory 8PM-11PM", - "format_ids": ["video_standard"], + "format_ids": ["video_15s", "video_30s"], "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 45.00 + "pricing_options": [ + { + "pricing_model": "cpm", + "rate": 45.00, + "currency": "USD", + "is_fixed": true, + "min_spend": 10000 + }, + { + "pricing_model": "cpcv", + "rate": 0.18, + "currency": "USD", + "is_fixed": true, + "min_spend": 10000 + }, + { + "pricing_model": "cpp", + "rate": 250.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "demographic": "A18-49", + "min_points": 50 + }, + "min_spend": 12500 + } + ] } ``` -### Custom Product +### Auction-Based Display Product ```json { "product_id": "custom_abc123", "name": "Custom - Gaming Enthusiasts", "description": "Custom audience package for gaming campaign", - "format_ids": ["display_300x250"], + "format_ids": ["display_300x250", "display_728x90"], "delivery_type": "non_guaranteed", - "is_fixed_price": false, - "price_guidance": { - "floor": 5.00, - "p50": 8.00, - "p75": 12.00 - }, + "pricing_options": [ + { + "pricing_model": "cpm", + "currency": "USD", + "is_fixed": false, + "price_guidance": { + "floor": 5.00, + "p50": 8.00, + "p75": 12.00 + } + }, + { + "pricing_model": "cpc", + "currency": "USD", + "is_fixed": false, + "price_guidance": { + "floor": 0.50, + "p50": 1.20, + "p75": 2.00 + } + } + ], "is_custom": true, - "expires_at": "2024-02-15T00:00:00Z" + "expires_at": "2025-02-15T00:00:00Z" } ``` -### Retail Media Product +### Retail Media Product with Measurement ```json { "product_id": "albertsons_pet_category_offsite", @@ -120,13 +188,30 @@ A server can offer a general catalog, but it can also return: "description": "Target Albertsons shoppers who have purchased pet products in the last 90 days. Reach them across premium display and video inventory.", "format_ids": [ "display_300x250", - "display_728x90", - "video_15s_vast" + "display_728x90", + "video_15s" ], "delivery_type": "guaranteed", - "is_fixed_price": true, - "cpm": 13.50, - "min_spend": 10000, + "pricing_options": [ + { + "pricing_model": "cpm", + "rate": 13.50, + "currency": "USD", + "is_fixed": true, + "min_spend": 10000 + }, + { + "pricing_model": "cpa", + "rate": 15.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "action_type": "purchase", + "attribution_window_days": 30 + }, + "min_spend": 10000 + } + ], "measurement": { "type": "incremental_sales_lift", "attribution": "deterministic_purchase", diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 03628b84a2..de8ec2a56f 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -30,10 +30,34 @@ Classification of signals as "marketplace" (third-party), "owned" (first-party), ## C -**CPM (Cost Per Mille)** -Pricing model based on cost per thousand impressions. +**CPA (Cost Per Action/Acquisition)** +Pricing model based on cost per conversion or acquisition event (e.g., purchase, sign-up). -**Customer Account** +**CPC (Cost Per Click)** +Pricing model based on cost per click on the advertisement. + +**CPCV (Cost Per Completed View)** +Pricing model based on cost per 100% video or audio completion. + +**CPL (Cost Per Lead)** +Pricing model based on cost per qualified lead generated. + +**CPM (Cost Per Mille)** +Pricing model based on cost per thousand impressions. Traditional display advertising pricing. + +**CPP (Cost Per Point)** +Pricing model based on cost per Gross Rating Point (GRP), commonly used in TV and audio advertising. + +**CPV (Cost Per View)** +Pricing model based on cost per view at a publisher-defined threshold (e.g., 50% video completion). + +**Completed View** +A video or audio ad that has been viewed to 100% completion. Used for CPCV pricing and completion rate metrics. + +**Completion Rate** +The percentage of video or audio ads that are viewed to 100% completion (completed_views / impressions). + +**Customer Account** Direct advertiser or agency account with specific seat access and negotiated rates. ## D @@ -52,12 +76,25 @@ Technology platform that allows advertisers to buy advertising inventory program ## E -**Estimated Activation Time** +**Estimated Activation Time** Predicted timeframe for signal deployment, typically 24-48 hours for new activations. +## F + +**Flat Rate** +Fixed-cost pricing model where a single payment is made regardless of delivery volume. Common for sponsorships and takeovers. + +**Frequency** +The average number of times an individual is exposed to an advertisement during a campaign. + +## G + +**GRP (Gross Rating Point)** +A unit of measurement for television and radio advertising representing 1% of the target audience. Used in CPP pricing models. Total GRPs = Reach % × Average Frequency. + ## H -**Households** +**Households** Size unit representing unique household addresses, useful for geographic and family-based targeting. ## I @@ -91,24 +128,38 @@ First-party signal data belonging to the advertiser or platform. ## P -**Platform Account** +**Platform Account** Master account representing an advertising platform that can syndicate signals to multiple customers. -**Prompt** +**Pricing Model** +The method by which advertising inventory is priced and billed. AdCP supports CPM, CPC, CPCV, CPV, CPA, CPL, CPP, and flat rate models. + +**Pricing Option** +A specific pricing model offered by a publisher for a product, including rate, currency, and parameters. + +**Prompt** Natural language description used to discover relevant signals (e.g., "high-income sports enthusiasts", "premium automotive content", "users in urban areas during evening hours"). -**Provider** +**Provider** The company or platform that supplies signal data (e.g., LiveRamp, Experian, Peer39, weather services). +## Q + +**Quartile (Video)** +Milestones in video ad viewing: Q1 (25% viewed), Q2 (50% viewed), Q3 (75% viewed), Q4 (100% complete). Used to measure video engagement. + ## R -**Relevance Score** +**Reach** +The number or percentage of unique individuals exposed to an advertisement at least once during a campaign. + +**Relevance Score** Numerical rating (0-1) indicating how well a signal matches the discovery prompt. -**Relevance Rationale** +**Relevance Rationale** Human-readable explanation of why an audience received its relevance score. -**Revenue Share** +**Revenue Share** Pricing model based on a percentage of media spend rather than fixed CPM. ## S diff --git a/static/schemas/v1/core/budget.json b/static/schemas/v1/core/budget.json index 2677034603..b8170215e7 100644 --- a/static/schemas/v1/core/budget.json +++ b/static/schemas/v1/core/budget.json @@ -2,12 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/core/budget.json", "title": "Budget", - "description": "Budget configuration for a media buy or package", + "description": "Budget configuration for a media buy or package. Budget is always expressed as a monetary amount - the pricing model determines what is being purchased.", "type": "object", "properties": { "total": { "type": "number", - "description": "Total budget amount", + "description": "Total budget amount in the specified currency", "minimum": 0 }, "currency": { diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json new file mode 100644 index 0000000000..7dfa041147 --- /dev/null +++ b/static/schemas/v1/core/pricing-option.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/pricing-option.json", + "title": "Pricing Option", + "description": "A pricing model option offered by a publisher for a product", + "type": "object", + "properties": { + "pricing_model": { + "$ref": "/schemas/v1/enums/pricing-model.json" + }, + "rate": { + "type": "number", + "description": "The rate for this pricing model (interpretation depends on pricing_model type)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "parameters": { + "type": "object", + "description": "Additional parameters specific to this pricing model", + "properties": { + "demographic": { + "type": "string", + "description": "Target demographic for CPP pricing (e.g., 'A18-49', 'W25-54')" + }, + "min_points": { + "type": "number", + "description": "Minimum GRPs/TRPs required for CPP pricing", + "minimum": 0 + }, + "view_threshold": { + "type": "number", + "description": "Percentage of video/audio that must be viewed for CPV pricing (0.0 to 1.0)", + "minimum": 0, + "maximum": 1 + }, + "action_type": { + "type": "string", + "description": "Type of action for CPA pricing (e.g., 'purchase', 'sign_up', 'download')" + }, + "attribution_window_days": { + "type": "integer", + "description": "Attribution window in days for CPA/CPL pricing", + "minimum": 1 + } + } + }, + "min_spend": { + "type": "number", + "description": "Minimum spend requirement for this pricing option in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_model", "currency", "is_fixed"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index a87bc3f2f7..17555e9223 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -46,24 +46,32 @@ "delivery_type": { "$ref": "/schemas/v1/enums/delivery-type.json" }, + "pricing_options": { + "type": "array", + "description": "Available pricing models for this product", + "items": { + "$ref": "/schemas/v1/core/pricing-option.json" + }, + "minItems": 1 + }, "is_fixed_price": { "type": "boolean", - "description": "Whether this product has fixed pricing (true) or uses auction (false)" + "description": "DEPRECATED: Use pricing_options instead. Whether this product has fixed pricing (true) or uses auction (false)" }, "cpm": { "type": "number", - "description": "Cost per thousand impressions", + "description": "DEPRECATED: Use pricing_options instead. Cost per thousand impressions", "minimum": 0 }, "currency": { "type": "string", - "description": "ISO 4217 currency code", + "description": "ISO 4217 currency code (for products using legacy cpm field)", "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP"] }, "min_spend": { "type": "number", - "description": "Minimum budget requirement", + "description": "DEPRECATED: Use pricing_options[].min_spend instead. Minimum budget requirement", "minimum": 0 }, "estimated_exposures": { @@ -73,12 +81,12 @@ }, "floor_cpm": { "type": "number", - "description": "Minimum CPM for non-guaranteed products (bids below this are rejected)", + "description": "DEPRECATED: Use pricing_options with price_guidance instead. Minimum CPM for non-guaranteed products", "minimum": 0 }, "recommended_cpm": { "type": "number", - "description": "Recommended CPM to achieve min_exposures target for non-guaranteed products", + "description": "DEPRECATED: Use pricing_options with price_guidance instead. Recommended CPM to achieve exposures target", "minimum": 0 }, "measurement": { @@ -104,12 +112,26 @@ "description": "Expiration timestamp for custom products" } }, - "oneOf": [ + "allOf": [ { - "required": ["properties"] + "oneOf": [ + { + "required": ["properties"] + }, + { + "required": ["property_tags"] + } + ] }, { - "required": ["property_tags"] + "oneOf": [ + { + "required": ["pricing_options"] + }, + { + "required": ["is_fixed_price"] + } + ] } ], "required": [ @@ -117,8 +139,7 @@ "name", "description", "format_ids", - "delivery_type", - "is_fixed_price" + "delivery_type" ], "additionalProperties": false } \ No newline at end of file diff --git a/static/schemas/v1/enums/pricing-model.json b/static/schemas/v1/enums/pricing-model.json new file mode 100644 index 0000000000..ea1597c58d --- /dev/null +++ b/static/schemas/v1/enums/pricing-model.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/pricing-model.json", + "title": "Pricing Model", + "description": "Supported pricing models for advertising products", + "type": "string", + "enum": [ + "cpm", + "cpc", + "cpcv", + "cpv", + "cpa", + "cpl", + "cpp", + "flat_rate" + ], + "enumDescriptions": { + "cpm": "Cost Per Mille - cost per 1,000 impressions", + "cpc": "Cost Per Click - cost per click on the ad", + "cpcv": "Cost Per Completed View - cost per 100% video/audio completion", + "cpv": "Cost Per View - cost per view at publisher-defined threshold (e.g., 50% completion)", + "cpa": "Cost Per Action/Acquisition - cost per conversion or acquisition event", + "cpl": "Cost Per Lead - cost per lead generated", + "cpp": "Cost Per Point - cost per Gross Rating Point or Target Rating Point (TV/audio)", + "flat_rate": "Flat Rate - fixed cost regardless of delivery volume (sponsorships, takeovers)" + } +} diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index 2e7da3cb7a..2b982dfe67 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -9,7 +9,7 @@ "versioning": { "note": "All request/response schemas include adcp_version field. Compatibility follows semantic versioning rules." }, - "lastUpdated": "2025-10-08", + "lastUpdated": "2025-10-12", "baseUrl": "/schemas/v1", "schemas": { "core": { @@ -98,12 +98,20 @@ "creative-manifest": { "$ref": "/schemas/v1/core/creative-manifest.json", "description": "Complete specification of a creative with all assets needed for rendering" + }, + "pricing-option": { + "$ref": "/schemas/v1/core/pricing-option.json", + "description": "A pricing model option offered by a publisher for a product" } } }, "enums": { "description": "Enumerated types and constants", "schemas": { + "pricing-model": { + "$ref": "/schemas/v1/enums/pricing-model.json", + "description": "Supported pricing models for advertising products" + }, "delivery-type": { "$ref": "/schemas/v1/enums/delivery-type.json", "description": "Type of inventory delivery" diff --git a/static/schemas/v1/media-buy/create-media-buy-request.json b/static/schemas/v1/media-buy/create-media-buy-request.json index e280869f28..6757c041da 100644 --- a/static/schemas/v1/media-buy/create-media-buy-request.json +++ b/static/schemas/v1/media-buy/create-media-buy-request.json @@ -9,7 +9,7 @@ "type": "string", "description": "AdCP schema version for this request", "pattern": "^\\d+\\.\\d+\\.\\d+$", - "default": "1.6.1" + "default": "1.7.0" }, "buyer_ref": { "type": "string", diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json index 99c3cd5136..15711a991e 100644 --- a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json @@ -122,6 +122,10 @@ "type": "boolean", "description": "Indicates this delivery contains updated data for a previously reported period. Buyer should replace previous period data with these totals." }, + "pricing_model": { + "$ref": "/schemas/v1/enums/pricing-model.json", + "description": "Pricing model used for this media buy" + }, "totals": { "type": "object", "description": "Aggregate metrics for this media buy across all packages", @@ -136,9 +140,14 @@ "description": "Total amount spent", "minimum": 0 }, + "effective_rate": { + "type": "number", + "description": "Effective rate paid per unit based on pricing_model (e.g., actual CPM for 'cpm', actual cost per completed view for 'cpcv', actual cost per point for 'cpp')", + "minimum": 0 + }, "clicks": { "type": "number", - "description": "Total clicks (if applicable)", + "description": "Total clicks (for CPC or general reporting)", "minimum": 0 }, "ctr": { @@ -147,20 +156,81 @@ "minimum": 0, "maximum": 1 }, + "views": { + "type": "number", + "description": "Total views at threshold (for CPV)", + "minimum": 0 + }, + "completed_views": { + "type": "number", + "description": "Total 100% completions (for CPCV)", + "minimum": 0 + }, "video_completions": { "type": "number", - "description": "Total video completions (if applicable)", + "description": "DEPRECATED: Use completed_views instead", "minimum": 0 }, "completion_rate": { "type": "number", - "description": "Video completion rate (completions/impressions)", + "description": "Completion rate (completed_views/impressions)", "minimum": 0, "maximum": 1 + }, + "conversions": { + "type": "number", + "description": "Total conversions (for CPA)", + "minimum": 0 + }, + "leads": { + "type": "number", + "description": "Total leads generated (for CPL)", + "minimum": 0 + }, + "grps": { + "type": "number", + "description": "Gross Rating Points delivered (for CPP)", + "minimum": 0 + }, + "reach": { + "type": "number", + "description": "Unique individuals reached", + "minimum": 0 + }, + "frequency": { + "type": "number", + "description": "Average frequency per individual", + "minimum": 0 + }, + "quartile_data": { + "type": "object", + "description": "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 + } + } } }, - "required": ["impressions", "spend"], - "additionalProperties": false + "required": ["spend"], + "additionalProperties": true }, "by_package": { "type": "array", @@ -191,9 +261,34 @@ "description": "Package clicks", "minimum": 0 }, + "views": { + "type": "number", + "description": "Package views (for CPV)", + "minimum": 0 + }, + "completed_views": { + "type": "number", + "description": "Package completed views (for CPCV)", + "minimum": 0 + }, "video_completions": { "type": "number", - "description": "Package video completions", + "description": "DEPRECATED: Use completed_views instead", + "minimum": 0 + }, + "conversions": { + "type": "number", + "description": "Package conversions (for CPA)", + "minimum": 0 + }, + "leads": { + "type": "number", + "description": "Package leads (for CPL)", + "minimum": 0 + }, + "grps": { + "type": "number", + "description": "Package GRPs (for CPP)", "minimum": 0 }, "pacing_index": { @@ -202,8 +297,8 @@ "minimum": 0 } }, - "required": ["package_id", "impressions", "spend"], - "additionalProperties": false + "required": ["package_id", "spend"], + "additionalProperties": true } }, "daily_breakdown": { diff --git a/static/schemas/v1/media-buy/package-request.json b/static/schemas/v1/media-buy/package-request.json index 0ae7cfcc47..619aa00fbf 100644 --- a/static/schemas/v1/media-buy/package-request.json +++ b/static/schemas/v1/media-buy/package-request.json @@ -33,7 +33,21 @@ } }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Budget allocation for this package in the media buy's currency", + "minimum": 0 + }, + "pacing": { + "$ref": "/schemas/v1/enums/pacing.json" + }, + "pricing_model": { + "$ref": "/schemas/v1/enums/pricing-model.json", + "description": "Selected pricing model from the product's available pricing_options (must be available in the media buy's currency)" + }, + "bid_price": { + "type": "number", + "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", + "minimum": 0 }, "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" @@ -47,8 +61,8 @@ } }, "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_ids"]}, - {"required": ["buyer_ref", "products", "format_ids"]} + {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_model"]}, + {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_model"]} ], "not": { "required": ["format_selection"] @@ -80,7 +94,21 @@ "description": "Dynamic format selection criteria" }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Budget allocation for this package in the media buy's currency", + "minimum": 0 + }, + "pacing": { + "$ref": "/schemas/v1/enums/pacing.json" + }, + "pricing_model": { + "$ref": "/schemas/v1/enums/pricing-model.json", + "description": "Selected pricing model from the product's available pricing_options (must be available in the media buy's currency)" + }, + "bid_price": { + "type": "number", + "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", + "minimum": 0 }, "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" @@ -94,8 +122,8 @@ } }, "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_selection"]}, - {"required": ["buyer_ref", "products", "format_selection"]} + {"required": ["buyer_ref", "product_id", "format_selection", "budget", "pricing_model"]}, + {"required": ["buyer_ref", "products", "format_selection", "budget", "pricing_model"]} ], "not": { "required": ["format_ids"] diff --git a/tests/schema-validation.test.js b/tests/schema-validation.test.js index 53707951eb..634394c999 100644 --- a/tests/schema-validation.test.js +++ b/tests/schema-validation.test.js @@ -272,22 +272,22 @@ async function runTests() { await test('Core schemas have appropriate required fields', () => { const coreSchemas = schemas.filter(([path]) => path.includes('/core/')); const requiredFieldChecks = { - 'product.json': ['product_id', 'name', 'description', 'format_ids', 'delivery_type', 'is_fixed_price'], + 'product.json': ['product_id', 'name', 'description', 'format_ids', 'delivery_type'], 'media-buy.json': ['media_buy_id', 'status', 'promoted_offering', 'total_budget', 'packages'], 'package.json': ['package_id', 'status'], 'creative-asset.json': ['creative_id', 'name', 'format'], 'error.json': ['code', 'message'], - 'budget.json': ['total', 'currency'] + 'budget.json': ['total', 'currency'] // Budget is always a monetary amount }; - + for (const [schemaPath, schema] of coreSchemas) { const filename = path.basename(schemaPath); const expectedRequired = requiredFieldChecks[filename]; - + if (expectedRequired) { const actualRequired = schema.required || []; const missing = expectedRequired.filter(field => !actualRequired.includes(field)); - + if (missing.length > 0) { return `${filename}: Missing required fields: ${missing.join(', ')}`; } From 6c9a94d89342d073fb010c5343536ad46b0455f0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 11:48:01 -0400 Subject: [PATCH 02/16] feat: add DOOH support and measurement transparency, remove CPA/CPL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extends non-impression pricing with DOOH support and addresses critical measurement source of truth concerns identified by the ad-tech-protocol-expert. ## DOOH Support Added - Add 7 DOOH-specific parameters to pricing-option.json: - duration_hours: Time-based pricing - sov_percentage: Share of voice allocation - loop_duration_seconds: Loop rotation duration - min_plays_per_hour: Frequency guarantee - venue_package: Named venue packages - estimated_impressions: Impression estimates - daypart: Daypart-specific targeting - Add DOOH delivery metrics to get-media-buy-delivery-response.json: - loop_plays: Number of loop plays - screens_used: Screen count - screen_time_seconds: Total display time - sov_achieved: Actual SOV delivered - venue_breakdown: Per-venue performance - calculation_basis: Transparency into impression calculations - Document DOOH use cases in pricing-models.md: - Times Square takeover example (flat_rate with 100% SOV) - Airport SOV package (CPM or flat_rate with 15% SOV) - Transit network hourly pricing ## Measurement & Source of Truth - Add delivery_measurement field to product.json: - impression_methodology: served, viewable_mrc, viewable_groupm, viewable_custom - viewability_threshold: Custom thresholds - viewability_provider: IAS, DoubleVerify, MOAT, publisher - dooh_calculation: Data source, audience measurement, update frequency - third_party_verification: Available providers and cost basis - Add "Measurement & Source of Truth" section to pricing-models.md: - Explicitly states publishers are authoritative source - Documents measurement methodologies - Provides DOOH transparency guidance - Best practices for publishers and buyers ## CPA/CPL Removed (Temporary) - Remove CPA and CPL from pricing-model enum - Update pricing-models.md to remove CPA/CPL sections - Update glossary to remove CPA/CPL entries - Mark conversions/leads metrics as "reserved for future use" **Rationale**: Ad-tech-protocol-expert identified that CPA/CPL are unimplementable without buyer-side conversion tracking mechanism. Rather than ship broken functionality, we're removing these models until proper conversion tracking is implemented. ## Schema Version - Bump adcp_version to 1.8.0 (breaking change: removed pricing models) - Update lastUpdated to 2025-10-12 ## Glossary Updates - Add DOOH terms: Daypart, DOOH, Loop Duration, Loop Plays, Screen Time, Share of Voice, Takeover, Time-Based Pricing, Venue, Venue Package - Remove CPA and CPL entries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 256 +++++++++++++++--- docs/reference/glossary.md | 58 ++-- static/schemas/v1/core/pricing-option.json | 34 +++ static/schemas/v1/core/product.json | 77 ++++++ static/schemas/v1/enums/pricing-model.json | 4 - static/schemas/v1/index.json | 2 +- .../media-buy/create-media-buy-request.json | 2 +- .../get-media-buy-delivery-response.json | 128 ++++++++- 8 files changed, 494 insertions(+), 67 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index fa63f8e531..ea6586255a 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -1,7 +1,7 @@ --- title: Pricing Models -description: Comprehensive guide to AdCP's flexible pricing models including CPM, CPCV, CPP, CPA, and more -keywords: [pricing models, CPM, CPCV, CPP, CPA, CPL, CPC, GRP, video pricing, performance pricing] +description: Comprehensive guide to AdCP's flexible pricing models including CPM, CPCV, CPP, CPC, and DOOH support +keywords: [pricing models, CPM, CPCV, CPP, CPC, CPV, GRP, video pricing, DOOH, share of voice, measurement] --- # Pricing Models @@ -24,6 +24,82 @@ AdCP supports multiple pricing models to accommodate different advertising chann - **Market Standards**: Each channel (TV, video, display, performance) can use its natural pricing unit - **Clear Expectations**: Both parties agree on pricing before campaign launch +## Measurement & Source of Truth + +### Publisher Authority + +In AdCP's direct publisher-buyer relationship model, **publishers are the authoritative source for delivery metrics** unless otherwise specified in the product's measurement declaration. This differs from programmatic advertising where multiple systems independently track metrics. + +**Key Principles:** + +1. **Publisher Measurement**: Publishers report impressions, views, clicks, GRPs, and other delivery metrics through `get_media_buy_delivery` +2. **Methodology Transparency**: Publishers should declare measurement methodology in `delivery_measurement` field +3. **Audit Rights**: Buyers should negotiate audit access and verification rights in contracts +4. **Third-Party Verification**: Publishers may optionally support third-party measurement providers (IAS, DoubleVerify, MOAT) + +### Measurement Methodologies + +Publishers declare how they count impressions via the `delivery_measurement` field in products: + +```json +{ + "product_id": "premium_video", + "delivery_measurement": { + "impression_methodology": "viewable_mrc", + "viewability_provider": "ias", + "third_party_verification": { + "available": true, + "providers": ["ias", "double_verify"], + "cost_basis": "cpm_addon" + } + } +} +``` + +**Impression Methodologies:** +- **served**: Ad request sent (traditional ad server counting) +- **viewable_mrc**: MRC standard (50% pixels, 1s display / 2s video) +- **viewable_groupm**: GroupM standard (100% pixels in view) +- **viewable_custom**: Publisher-defined threshold + +### DOOH Measurement Transparency + +For DOOH campaigns, publishers should provide calculation transparency via `calculation_basis`: + +```json +{ + "dooh_metrics": { + "loop_plays": 4800, + "screens_used": 20, + "impressions": 75000, + "calculation_basis": { + "total_loop_opportunities": 32000, + "venue_traffic_estimate": 250000, + "impression_multiplier": 15.625, + "data_source": "geopath_december_2024" + } + } +} +``` + +**Formula**: `impressions = loop_plays × impression_multiplier` + +This transparency allows buyers to understand and validate impression calculations. + +### Best Practices + +**For Publishers:** +- Declare measurement methodology clearly in products +- Provide calculation transparency for DOOH +- Consider offering third-party verification for premium inventory +- Maintain detailed audit logs for potential disputes + +**For Buyers:** +- Review publisher measurement methodology before committing budget +- Negotiate audit rights in contracts +- Request third-party verification for large campaigns +- For DOOH, validate impression calculations using `calculation_basis` + ## Supported Pricing Models ### CPM (Cost Per Mille) @@ -150,71 +226,171 @@ AdCP supports multiple pricing models to accommodate different advertising chann --- -### CPA (Cost Per Action/Acquisition) -**Cost per conversion** - Payment based on completed actions or acquisitions. +### Flat Rate +**Fixed cost** - Single payment regardless of delivery volume. -**Use Cases**: E-commerce, lead generation, app installs, performance marketing +**Use Cases**: Sponsorships, takeovers, exclusive placements, branded content **Example**: ```json { - "pricing_model": "cpa", - "rate": 25.00, + "pricing_model": "flat_rate", + "rate": 50000.00, "currency": "USD", - "is_fixed": true, - "parameters": { - "action_type": "purchase", - "attribution_window_days": 30 - } + "is_fixed": true } ``` -**Billing**: Charged per conversion/acquisition event - -**Parameters**: -- `action_type`: Type of action (e.g., "purchase", "sign_up", "download", "add_to_cart") -- `attribution_window_days`: Attribution window (e.g., 7, 14, 30 days) +**Billing**: Fixed cost for the entire campaign period --- -### CPL (Cost Per Lead) -**Cost per lead generated** - Payment for qualified leads. +## Digital Out-of-Home (DOOH) Pricing -**Use Cases**: B2B marketing, form fills, newsletter signups, quote requests +DOOH advertising uses existing pricing models (CPM, flat_rate) enhanced with DOOH-specific parameters. Common buying modes like Share of Voice (SOV), time-based buying, and takeovers are **inventory allocation mechanisms**, not separate pricing models. + +### Key Concept: Inventory Allocation vs. Pricing + +- **Share of Voice**: Percentage of loop plays → Priced as CPM or flat_rate with `sov_percentage` parameter +- **Time-based**: Hourly/daily/daypart booking → Priced as flat_rate with `duration_hours` parameter +- **Takeover**: 100% exclusive placement → Priced as flat_rate with `sov_percentage: 100` + +### DOOH Use Case: Times Square Takeover + +**24-hour exclusive takeover of premium billboard:** -**Example**: ```json { - "pricing_model": "cpl", - "rate": 50.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "attribution_window_days": 7 - } + "product_id": "times_square_digital_takeover", + "name": "Times Square Digital Takeover", + "description": "Exclusive 24-hour takeover of premium Times Square digital billboard", + "format_ids": ["dooh_portrait_1920x1080"], + "delivery_type": "guaranteed", + "pricing_options": [ + { + "pricing_model": "flat_rate", + "rate": 50000.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "duration_hours": 24, + "sov_percentage": 100, + "loop_duration_seconds": 300, + "min_plays_per_hour": 12, + "venue_package": "times_square_premium" + } + } + ] } ``` -**Billing**: Charged per qualified lead generated +**Buyer selection:** +```json +{ + "packages": [{ + "buyer_ref": "pkg_launch_takeover", + "products": ["times_square_digital_takeover"], + "format_ids": ["dooh_portrait_1920x1080"], + "budget": 50000, + "pricing_model": "flat_rate" + }] +} +``` ---- +### DOOH Use Case: Airport SOV Package -### Flat Rate -**Fixed cost** - Single payment regardless of delivery volume. +**15% share of voice during morning commute with dual pricing options:** -**Use Cases**: Sponsorships, takeovers, exclusive placements, branded content +```json +{ + "product_id": "airport_morning_commute", + "name": "Airport Morning Commute Package", + "description": "15% share of voice during morning rush (5am-9am)", + "format_ids": ["dooh_portrait_1080x1920"], + "delivery_type": "guaranteed", + "pricing_options": [ + { + "pricing_model": "cpm", + "rate": 25.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "sov_percentage": 15, + "loop_duration_seconds": 180, + "daypart": "morning_commute", + "estimated_impressions": 75000 + }, + "min_spend": 1875 + }, + { + "pricing_model": "flat_rate", + "rate": 2000.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "duration_hours": 4, + "sov_percentage": 15, + "loop_duration_seconds": 180, + "daypart": "morning_commute" + } + } + ] +} +``` + +**Notice:** Same inventory, two pricing options. Buyers choose based on whether they prefer impression-based (CPM) or time-based (flat_rate) pricing. + +### DOOH Use Case: Transit Network Hourly + +**Hourly pricing for retail transit locations:** -**Example**: ```json { - "pricing_model": "flat_rate", - "rate": 50000.00, - "currency": "USD", - "is_fixed": true + "product_id": "retail_transit_hourly", + "name": "Retail Transit Network - Hourly", + "description": "Hourly booking across 50 retail transit locations", + "format_ids": ["dooh_landscape_1920x1080"], + "delivery_type": "guaranteed", + "pricing_options": [ + { + "pricing_model": "flat_rate", + "rate": 500.00, + "currency": "USD", + "is_fixed": true, + "parameters": { + "duration_hours": 1, + "venue_package": "retail_transit_50_screens", + "loop_duration_seconds": 240, + "min_plays_per_hour": 15 + } + } + ] } ``` -**Billing**: Fixed cost for the entire campaign period +### DOOH Parameters Reference + +| Parameter | Type | Description | Use With | +|-----------|------|-------------|----------| +| `duration_hours` | number | Duration for time-based pricing | flat_rate | +| `sov_percentage` | number | Share of voice (0-100) | cpm, flat_rate | +| `loop_duration_seconds` | integer | Ad loop rotation duration | cpm, flat_rate | +| `min_plays_per_hour` | integer | Minimum frequency guarantee | cpm, flat_rate | +| `venue_package` | string | Named venue package ID | any | +| `estimated_impressions` | integer | Expected impressions | cpm, flat_rate | +| `daypart` | string | Specific daypart targeting | any | + +### DOOH Delivery Metrics + +When reporting on DOOH campaigns, publishers should include DOOH-specific metrics: + +- `loop_plays`: Number of times ad played in rotation +- `screens_used`: Number of unique screens displaying the ad +- `screen_time_seconds`: Total display time across all screens +- `sov_achieved`: Actual share of voice delivered (0.0-1.0) +- `venue_breakdown`: Per-venue performance data + +See [Delivery Reporting](../task-reference/get_media_buy_delivery.md) for full metric definitions. --- @@ -325,8 +501,6 @@ Different pricing models report different primary metrics: | CPV | views | impressions, quartile_data, spend | | CPP | grps | reach, frequency, spend | | CPC | clicks | impressions, ctr, spend | -| CPA | conversions | impressions, clicks, spend | -| CPL | leads | impressions, clicks, spend | | Flat Rate | N/A | impressions, reach, frequency | ## Example: Multi-Model CTV Product diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index de8ec2a56f..b88c132799 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -30,18 +30,12 @@ Classification of signals as "marketplace" (third-party), "owned" (first-party), ## C -**CPA (Cost Per Action/Acquisition)** -Pricing model based on cost per conversion or acquisition event (e.g., purchase, sign-up). - **CPC (Cost Per Click)** Pricing model based on cost per click on the advertisement. **CPCV (Cost Per Completed View)** Pricing model based on cost per 100% video or audio completion. -**CPL (Cost Per Lead)** -Pricing model based on cost per qualified lead generated. - **CPM (Cost Per Mille)** Pricing model based on cost per thousand impressions. Traditional display advertising pricing. @@ -62,16 +56,22 @@ Direct advertiser or agency account with specific seat access and negotiated rat ## D -**Deployment** (Signals Protocol) +**Daypart** +Specific time-of-day segment for time-based advertising, commonly used in DOOH (e.g., morning_commute, evening_prime, overnight). + +**Deployment** (Signals Protocol) The availability status of a signal on specific platforms, including activation state and timing. -**Devices** (Signals Protocol) +**Devices** (Signals Protocol) Size unit representing unique device identifiers (cookies, mobile IDs) - typically the largest reach metric. -**Device Type** (Media Buy Protocol) +**Device Type** (Media Buy Protocol) Targeting dimension for platform types: mobile, desktop, tablet, CTV, audio, DOOH. -**DSP (Demand-Side Platform)** +**DOOH (Digital Out-of-Home)** +Digital advertising displayed on screens in public spaces such as billboards, transit stations, airports, and retail locations. Uses CPM or flat_rate pricing with parameters for SOV, duration, and venue targeting. + +**DSP (Demand-Side Platform)** Technology platform that allows advertisers to buy advertising inventory programmatically. ## E @@ -164,25 +164,45 @@ Pricing model based on a percentage of media spend rather than fixed CPM. ## S -**Seat** +**Screen Time** +Total duration an ad was displayed across all DOOH screens, measured in seconds. Used for DOOH delivery reporting. + +**Seat** A specific advertising account within a platform, typically representing a brand or campaign. -**Segment ID** +**Segment ID** The specific identifier used for signal activation, may differ from signal_id. -**Size Unit** (Signals Protocol) +**Share of Voice (SOV)** +Percentage of available ad inventory allocated to a specific advertiser in DOOH loops. Expressed as 0.0-1.0 (e.g., 0.15 = 15% SOV). + +**Size Unit** (Signals Protocol) The measurement type for signal size: individuals, devices, or households. ## T -**Third-Party Signal** +**Takeover** +Exclusive 100% share of voice placement on DOOH inventory for a specific time period. Priced as flat_rate with sov_percentage: 100. + +**Third-Party Signal** Signal data licensed from external providers, also known as marketplace signals. +**Time-Based Pricing** +Pricing structure based on duration (hourly, daily, or daypart) rather than impressions. Common in DOOH advertising using flat_rate model. + ## U -**Usage Reporting** +**Usage Reporting** Daily reporting of signal utilization for billing and optimization purposes. +## V + +**Venue** +Physical location where DOOH advertising is displayed (e.g., airport terminal, transit station, retail store). Used for DOOH targeting and delivery reporting. + +**Venue Package** +Named collection of DOOH screens across specific venues (e.g., 'times_square_network', 'airport_terminals'). Used in DOOH pricing parameters. + ## B **Budget** @@ -203,9 +223,15 @@ Protocol feature allowing publishers to require manual approval for operations. ## L -**Line Item** +**Line Item** The basic unit of inventory in ad servers like Google Ad Manager, represented as packages in AdCP. +**Loop Duration** +The length of time for a complete rotation of ads in a DOOH display, measured in seconds. Used to calculate frequency and share of voice. + +**Loop Plays** +The number of times an ad was displayed in a DOOH loop rotation. Key metric for DOOH delivery reporting. + ## M **MCP (Model Context Protocol)** diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 7dfa041147..1af4a8acf9 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -82,6 +82,40 @@ "type": "integer", "description": "Attribution window in days for CPA/CPL pricing", "minimum": 1 + }, + "duration_hours": { + "type": "number", + "description": "Duration in hours for time-based flat rate pricing (DOOH)", + "minimum": 0 + }, + "sov_percentage": { + "type": "number", + "description": "Guaranteed share of voice as percentage (DOOH, 0-100)", + "minimum": 0, + "maximum": 100 + }, + "loop_duration_seconds": { + "type": "integer", + "description": "Duration of ad loop rotation in seconds (DOOH)", + "minimum": 1 + }, + "min_plays_per_hour": { + "type": "integer", + "description": "Minimum number of times ad plays per hour (DOOH frequency guarantee)", + "minimum": 0 + }, + "venue_package": { + "type": "string", + "description": "Named venue package identifier for DOOH (e.g., 'times_square_network', 'airport_terminals')" + }, + "estimated_impressions": { + "type": "integer", + "description": "Estimated impressions for this pricing option (informational, commonly used with SOV or time-based DOOH)", + "minimum": 0 + }, + "daypart": { + "type": "string", + "description": "Specific daypart for time-based pricing (e.g., 'morning_commute', 'evening_prime', 'overnight')" } } }, diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index 17555e9223..43e2d96840 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -92,6 +92,83 @@ "measurement": { "$ref": "/schemas/v1/core/measurement.json" }, + "delivery_measurement": { + "type": "object", + "description": "Methodology for measuring basic delivery metrics (impressions, views, etc.)", + "properties": { + "impression_methodology": { + "type": "string", + "enum": ["served", "viewable_mrc", "viewable_groupm", "viewable_custom"], + "description": "How impressions are counted: served (ad request), viewable_mrc (50% pixels 1s display/2s video), viewable_groupm (100% pixels), or viewable_custom (publisher-defined)" + }, + "viewability_threshold": { + "type": "object", + "description": "Custom viewability threshold (required if impression_methodology is viewable_custom)", + "properties": { + "pixel_percentage": { + "type": "number", + "description": "Percentage of pixels that must be visible (0.0 to 1.0)", + "minimum": 0, + "maximum": 1 + }, + "duration_seconds": { + "type": "number", + "description": "Minimum duration in seconds", + "minimum": 0 + } + } + }, + "viewability_provider": { + "type": "string", + "description": "Third-party viewability measurement provider", + "examples": ["ias", "double_verify", "moat", "publisher"] + }, + "dooh_calculation": { + "type": "object", + "description": "DOOH-specific impression calculation methodology", + "properties": { + "data_source": { + "type": "string", + "description": "Audience measurement data source", + "examples": ["geopath", "place_exchange", "vistar", "proprietary"] + }, + "audience_measurement": { + "type": "string", + "description": "How venue audience is measured", + "examples": ["venue_traffic_avg", "sensor_based", "mobile_data"] + }, + "update_frequency": { + "type": "string", + "description": "How often audience data is updated", + "examples": ["real_time", "daily", "weekly", "monthly", "quarterly"] + } + } + }, + "third_party_verification": { + "type": "object", + "description": "Third-party verification options available to buyers", + "properties": { + "available": { + "type": "boolean", + "description": "Whether third-party verification is available" + }, + "providers": { + "type": "array", + "description": "Available verification providers", + "items": { + "type": "string", + "examples": ["ias", "double_verify", "moat"] + } + }, + "cost_basis": { + "type": "string", + "description": "How verification costs are charged", + "examples": ["included", "cpm_addon", "flat_fee"] + } + } + } + } + }, "reporting_capabilities": { "$ref": "/schemas/v1/core/reporting-capabilities.json" }, diff --git a/static/schemas/v1/enums/pricing-model.json b/static/schemas/v1/enums/pricing-model.json index ea1597c58d..822e72f66a 100644 --- a/static/schemas/v1/enums/pricing-model.json +++ b/static/schemas/v1/enums/pricing-model.json @@ -9,8 +9,6 @@ "cpc", "cpcv", "cpv", - "cpa", - "cpl", "cpp", "flat_rate" ], @@ -19,8 +17,6 @@ "cpc": "Cost Per Click - cost per click on the ad", "cpcv": "Cost Per Completed View - cost per 100% video/audio completion", "cpv": "Cost Per View - cost per view at publisher-defined threshold (e.g., 50% completion)", - "cpa": "Cost Per Action/Acquisition - cost per conversion or acquisition event", - "cpl": "Cost Per Lead - cost per lead generated", "cpp": "Cost Per Point - cost per Gross Rating Point or Target Rating Point (TV/audio)", "flat_rate": "Flat Rate - fixed cost regardless of delivery volume (sponsorships, takeovers)" } diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index 2b982dfe67..107d0243f7 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -4,7 +4,7 @@ "title": "AdCP Schema Registry v1", "version": "1.0.0", "description": "Registry of all AdCP JSON schemas for validation and discovery", - "adcp_version": "1.7.0", + "adcp_version": "1.8.0", "standard_formats_version": "1.0.0", "versioning": { "note": "All request/response schemas include adcp_version field. Compatibility follows semantic versioning rules." diff --git a/static/schemas/v1/media-buy/create-media-buy-request.json b/static/schemas/v1/media-buy/create-media-buy-request.json index 6757c041da..aa57c3bdf7 100644 --- a/static/schemas/v1/media-buy/create-media-buy-request.json +++ b/static/schemas/v1/media-buy/create-media-buy-request.json @@ -9,7 +9,7 @@ "type": "string", "description": "AdCP schema version for this request", "pattern": "^\\d+\\.\\d+\\.\\d+$", - "default": "1.7.0" + "default": "1.8.0" }, "buyer_ref": { "type": "string", diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json index 15711a991e..143bd087c2 100644 --- a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json @@ -179,12 +179,12 @@ }, "conversions": { "type": "number", - "description": "Total conversions (for CPA)", + "description": "Total conversions (reserved for future CPA pricing support)", "minimum": 0 }, "leads": { "type": "number", - "description": "Total leads generated (for CPL)", + "description": "Total leads generated (reserved for future CPL pricing support)", "minimum": 0 }, "grps": { @@ -227,6 +227,98 @@ "minimum": 0 } } + }, + "dooh_metrics": { + "type": "object", + "description": "DOOH-specific delivery metrics (only included for DOOH campaigns)", + "properties": { + "loop_plays": { + "type": "integer", + "description": "Number of times ad played in rotation across all screens", + "minimum": 0 + }, + "screens_used": { + "type": "integer", + "description": "Number of unique screens displaying the ad", + "minimum": 0 + }, + "screen_time_seconds": { + "type": "integer", + "description": "Total time ad was displayed across all screens in seconds", + "minimum": 0 + }, + "sov_achieved": { + "type": "number", + "description": "Actual share of voice delivered as decimal (0.0 to 1.0, e.g., 0.15 = 15%)", + "minimum": 0, + "maximum": 1 + }, + "calculation_basis": { + "type": "object", + "description": "Transparency into how DOOH metrics were calculated", + "properties": { + "total_loop_opportunities": { + "type": "integer", + "description": "Total number of loop plays available during campaign period", + "minimum": 0 + }, + "venue_traffic_estimate": { + "type": "integer", + "description": "Estimated number of people who passed by screens", + "minimum": 0 + }, + "impression_multiplier": { + "type": "number", + "description": "Average impressions per loop play based on venue traffic", + "minimum": 0 + }, + "data_source": { + "type": "string", + "description": "Source of audience measurement data with date", + "examples": ["geopath_december_2024", "vistar_q4_2024"] + } + } + }, + "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": false + } + } + }, + "additionalProperties": false } }, "required": ["spend"], @@ -278,12 +370,12 @@ }, "conversions": { "type": "number", - "description": "Package conversions (for CPA)", + "description": "Package conversions (reserved for future CPA pricing support)", "minimum": 0 }, "leads": { "type": "number", - "description": "Package leads (for CPL)", + "description": "Package leads (reserved for future CPL pricing support)", "minimum": 0 }, "grps": { @@ -295,6 +387,34 @@ "type": "number", "description": "Delivery pace (1.0 = on track, <1.0 = behind, >1.0 = ahead)", "minimum": 0 + }, + "dooh_metrics": { + "type": "object", + "description": "DOOH-specific metrics for this package (only included for DOOH campaigns)", + "properties": { + "loop_plays": { + "type": "integer", + "description": "Loop plays for this package", + "minimum": 0 + }, + "screens_used": { + "type": "integer", + "description": "Screens used for this package", + "minimum": 0 + }, + "screen_time_seconds": { + "type": "integer", + "description": "Screen time for this package in seconds", + "minimum": 0 + }, + "sov_achieved": { + "type": "number", + "description": "Actual SOV for this package (0.0 to 1.0)", + "minimum": 0, + "maximum": 1 + } + }, + "additionalProperties": false } }, "required": ["package_id", "spend"], From fdaa79a78126f5c527a9b2ff263aafb3478ab888 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 12:50:12 -0400 Subject: [PATCH 03/16] docs: clarify CPP measurement requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add explicit measurement requirements for CPP pricing to address concerns about GRP measurement credibility. ## Changes - Add "Measurement Requirements" section to CPP documentation - List accepted measurement sources (Nielsen, Comscore, iSpot, Triton) - Example showing delivery_measurement declaration - Warning that CPP without certified measurement is not credible - Update GRP glossary entry to list third-party measurement providers ## Rationale Unlike CPA/CPL (which require buyer-side conversion tracking), CPP is measurable via industry-standard third-party validators. Publishers offering CPP should declare their measurement source, and buyers should verify certification before accepting deals. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 25 +++++++++++++++++++ docs/reference/glossary.md | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index ea6586255a..6e5ea65b3b 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -200,6 +200,31 @@ This transparency allows buyers to understand and validate impression calculatio - `reach`: Unique individuals reached - `frequency`: Average frequency per individual +**Measurement Requirements**: + +CPP pricing requires **certified demographic measurement**. Publishers should declare their GRP measurement source via `delivery_measurement`: + +```json +{ + "pricing_model": "cpp", + "rate": 250.00, + "delivery_measurement": { + "source": "nielsen_dar", + "demographic_measurement": "panel_based", + "reporting_cadence": "weekly" + } +} +``` + +**Accepted Measurement Sources**: +- **Nielsen DAR/TV**: Industry-standard TV measurement +- **Comscore**: Campaign Ratings for CTV +- **iSpot.tv**: Advanced TV analytics +- **Triton Digital**: Audio/streaming measurement +- **Publisher-certified panels**: Large platforms with certified audience measurement + +Buyers should verify publisher's measurement certification before accepting CPP deals. Without certified measurement, GRP claims are not credible. + --- ### CPC (Cost Per Click) diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index b88c132799..daabe732f8 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -90,7 +90,7 @@ The average number of times an individual is exposed to an advertisement during ## G **GRP (Gross Rating Point)** -A unit of measurement for television and radio advertising representing 1% of the target audience. Used in CPP pricing models. Total GRPs = Reach % × Average Frequency. +A unit of measurement for television and radio advertising representing 1% of the target audience. Used in CPP pricing models. Total GRPs = Reach % × Average Frequency. Measured by third-party providers such as Nielsen, Comscore, iSpot.tv, or Triton Digital. ## H From 436af1e98aaa650b44b97470adb31403a8a18b5e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 14:16:32 -0400 Subject: [PATCH 04/16] refactor: simplify measurement to use unstructured text fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address user feedback that structured measurement fields defeat the purpose of AI/LLM processing. Simplify to natural language fields. ## Key Changes 1. **Product measurement simplified**: - Replace complex structured fields with simple `provider` + `notes` - Example: "Google Ad Manager with IAS viewability verification" - LLMs can parse natural language better than rigid schemas 2. **DOOH calculation transparency**: - Replace structured `calculation_basis` object with `calculation_notes` text - Example: "Impressions based on Geopath December 2024 venue traffic data" - Humans and LLMs both prefer plain language explanation 3. **Documentation rewrite**: - Focus on "measurement provider as source of truth" concept - Buyer accepts the declared provider when accepting the product - Remove complex methodology enums (served, viewable_mrc, etc.) ## User Feedback Addressed - Comment #1: Product should specify measurement provider, buyer accepts it - Comment #2: Separate provider (WHO) from methodology (HOW) - both in text - Comment #3: Don't use structured data for LLM-parseable content - Comment #4: "geopath_december_2024" works fine as text, no need for fields ## Rationale AdCP is designed for AI agents to process natural language. Over-structured schemas make it harder for LLMs to understand and work with the data. Simple text fields allow publishers to explain their measurement in their own words. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 85 +++++-------------- static/schemas/v1/core/product.json | 73 ++-------------- .../get-media-buy-delivery-response.json | 28 +----- 3 files changed, 31 insertions(+), 155 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index 6e5ea65b3b..1e50d26df7 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -26,79 +26,42 @@ AdCP supports multiple pricing models to accommodate different advertising chann ## Measurement & Source of Truth -### Publisher Authority +### Measurement Provider as Source of Truth -In AdCP's direct publisher-buyer relationship model, **publishers are the authoritative source for delivery metrics** unless otherwise specified in the product's measurement declaration. This differs from programmatic advertising where multiple systems independently track metrics. +**The product declares the measurement provider, and the buyer accepts that provider as the source of truth for the buy.** -**Key Principles:** - -1. **Publisher Measurement**: Publishers report impressions, views, clicks, GRPs, and other delivery metrics through `get_media_buy_delivery` -2. **Methodology Transparency**: Publishers should declare measurement methodology in `delivery_measurement` field -3. **Audit Rights**: Buyers should negotiate audit access and verification rights in contracts -4. **Third-Party Verification**: Publishers may optionally support third-party measurement providers (IAS, DoubleVerify, MOAT) - -### Measurement Methodologies - -Publishers declare how they count impressions via the `delivery_measurement` field in products: +Publishers specify their measurement provider in the product: ```json { "product_id": "premium_video", "delivery_measurement": { - "impression_methodology": "viewable_mrc", - "viewability_provider": "ias", - "third_party_verification": { - "available": true, - "providers": ["ias", "double_verify"], - "cost_basis": "cpm_addon" - } - } -} -``` - -**Impression Methodologies:** -- **served**: Ad request sent (traditional ad server counting) -- **viewable_mrc**: MRC standard (50% pixels, 1s display / 2s video) -- **viewable_groupm**: GroupM standard (100% pixels in view) -- **viewable_custom**: Publisher-defined threshold - -### DOOH Measurement Transparency - -For DOOH campaigns, publishers should provide calculation transparency via `calculation_basis`: - -```json -{ - "dooh_metrics": { - "loop_plays": 4800, - "screens_used": 20, - "impressions": 75000, - "calculation_basis": { - "total_loop_opportunities": 32000, - "venue_traffic_estimate": 250000, - "impression_multiplier": 15.625, - "data_source": "geopath_december_2024" - } + "provider": "Google Ad Manager with IAS viewability verification", + "notes": "MRC-accredited viewability measurement. 50% in-view for 1 second (display) or 2 seconds (video)." } } ``` -**Formula**: `impressions = loop_plays × impression_multiplier` +**Common Measurement Providers:** +- **Ad Servers**: Google Ad Manager, Freewheel, SpringServe +- **Attention Metrics**: Adelaide, Lumen, TVision +- **Third-Party Verification**: IAS, DoubleVerify, MOAT +- **TV/Audio Measurement**: Nielsen, Comscore, iSpot.tv, Triton Digital +- **DOOH**: Geopath, Vistar, Place Exchange -This transparency allows buyers to understand and validate impression calculations. +By accepting the product, buyers agree to use the declared measurement provider as the authoritative source for delivery metrics. ### Best Practices **For Publishers:** -- Declare measurement methodology clearly in products -- Provide calculation transparency for DOOH -- Consider offering third-party verification for premium inventory -- Maintain detailed audit logs for potential disputes +- Clearly identify your measurement provider (ad server and any third-party verification) +- Explain your measurement methodology in plain language +- For DOOH, specify your audience measurement source (e.g., Geopath, venue sensors) **For Buyers:** -- Review publisher measurement methodology before committing budget -- Negotiate audit rights in contracts -- Request third-party verification for large campaigns -- For DOOH, validate impression calculations using `calculation_basis` +- Review the measurement provider before committing budget +- Ensure the provider meets your campaign requirements +- Negotiate audit rights in contracts if needed ## Supported Pricing Models @@ -202,28 +165,26 @@ This transparency allows buyers to understand and validate impression calculatio **Measurement Requirements**: -CPP pricing requires **certified demographic measurement**. Publishers should declare their GRP measurement source via `delivery_measurement`: +CPP pricing requires **certified demographic measurement**. Publishers should declare their measurement provider: ```json { "pricing_model": "cpp", "rate": 250.00, "delivery_measurement": { - "source": "nielsen_dar", - "demographic_measurement": "panel_based", - "reporting_cadence": "weekly" + "provider": "Nielsen DAR", + "notes": "Panel-based demographic measurement for A18-49. GRP reports available weekly." } } ``` -**Accepted Measurement Sources**: +**Common Measurement Providers for CPP**: - **Nielsen DAR/TV**: Industry-standard TV measurement - **Comscore**: Campaign Ratings for CTV - **iSpot.tv**: Advanced TV analytics - **Triton Digital**: Audio/streaming measurement -- **Publisher-certified panels**: Large platforms with certified audience measurement -Buyers should verify publisher's measurement certification before accepting CPP deals. Without certified measurement, GRP claims are not credible. +Buyers should verify the measurement provider meets their campaign requirements before accepting CPP deals. --- diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index 43e2d96840..46b8b46cd0 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -94,78 +94,15 @@ }, "delivery_measurement": { "type": "object", - "description": "Methodology for measuring basic delivery metrics (impressions, views, etc.)", + "description": "Measurement provider and methodology for delivery metrics. The buyer accepts the declared provider as the source of truth for the buy.", "properties": { - "impression_methodology": { + "provider": { "type": "string", - "enum": ["served", "viewable_mrc", "viewable_groupm", "viewable_custom"], - "description": "How impressions are counted: served (ad request), viewable_mrc (50% pixels 1s display/2s video), viewable_groupm (100% pixels), or viewable_custom (publisher-defined)" + "description": "Measurement provider(s) used for this product (e.g., 'Google Ad Manager with IAS viewability', 'Nielsen DAR', 'Geopath for DOOH impressions')" }, - "viewability_threshold": { - "type": "object", - "description": "Custom viewability threshold (required if impression_methodology is viewable_custom)", - "properties": { - "pixel_percentage": { - "type": "number", - "description": "Percentage of pixels that must be visible (0.0 to 1.0)", - "minimum": 0, - "maximum": 1 - }, - "duration_seconds": { - "type": "number", - "description": "Minimum duration in seconds", - "minimum": 0 - } - } - }, - "viewability_provider": { + "notes": { "type": "string", - "description": "Third-party viewability measurement provider", - "examples": ["ias", "double_verify", "moat", "publisher"] - }, - "dooh_calculation": { - "type": "object", - "description": "DOOH-specific impression calculation methodology", - "properties": { - "data_source": { - "type": "string", - "description": "Audience measurement data source", - "examples": ["geopath", "place_exchange", "vistar", "proprietary"] - }, - "audience_measurement": { - "type": "string", - "description": "How venue audience is measured", - "examples": ["venue_traffic_avg", "sensor_based", "mobile_data"] - }, - "update_frequency": { - "type": "string", - "description": "How often audience data is updated", - "examples": ["real_time", "daily", "weekly", "monthly", "quarterly"] - } - } - }, - "third_party_verification": { - "type": "object", - "description": "Third-party verification options available to buyers", - "properties": { - "available": { - "type": "boolean", - "description": "Whether third-party verification is available" - }, - "providers": { - "type": "array", - "description": "Available verification providers", - "items": { - "type": "string", - "examples": ["ias", "double_verify", "moat"] - } - }, - "cost_basis": { - "type": "string", - "description": "How verification costs are charged", - "examples": ["included", "cpm_addon", "flat_fee"] - } - } + "description": "Additional details about measurement methodology in plain language (e.g., 'MRC-accredited viewability. 50% in-view for 1s display / 2s video', 'Panel-based demographic measurement updated monthly')" } } }, diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json index 143bd087c2..e63f6af31a 100644 --- a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json @@ -253,31 +253,9 @@ "minimum": 0, "maximum": 1 }, - "calculation_basis": { - "type": "object", - "description": "Transparency into how DOOH metrics were calculated", - "properties": { - "total_loop_opportunities": { - "type": "integer", - "description": "Total number of loop plays available during campaign period", - "minimum": 0 - }, - "venue_traffic_estimate": { - "type": "integer", - "description": "Estimated number of people who passed by screens", - "minimum": 0 - }, - "impression_multiplier": { - "type": "number", - "description": "Average impressions per loop play based on venue traffic", - "minimum": 0 - }, - "data_source": { - "type": "string", - "description": "Source of audience measurement data with date", - "examples": ["geopath_december_2024", "vistar_q4_2024"] - } - } + "calculation_notes": { + "type": "string", + "description": "Explanation of how DOOH impressions were calculated (e.g., 'Impressions based on Geopath December 2024 venue traffic data. 4,800 loop plays × 15.6 avg impressions per play = 75,000 total impressions.')" }, "venue_breakdown": { "type": "array", From 2cebb012b549d2b5dae6da7455dd90f1e88d393d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 14:26:52 -0400 Subject: [PATCH 05/16] docs: simplify DOOH section and clarify details per feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address user feedback to keep DOOH documentation basic since we're not experts. DOOH experts can refine later. ## Changes 1. **Replace MOAT with Scope3** in measurement providers list - MOAT is out of business 2. **Clarify min_spend scope** - Updated schema: "Minimum spend requirement per package" - Was ambiguous whether per package or per media buy 3. **Remove redundant view_threshold for CPCV** - CPCV = completed view (100% by definition) - view_threshold: 1.0 was redundant - Add note: "Completion is measured by the declared measurement provider" 4. **Drastically simplify DOOH section** - Remove complex examples (Times Square takeover, Airport SOV, Transit) - Remove DOOH parameters reference table - Remove DOOH delivery metrics list - Replace with: Basic concepts + one simple example - Add disclaimer: "DOOH measurement varies by market. Publishers should clearly explain their methodology." ## Rationale We're not DOOH experts. Cover the basics (CPM and flat_rate work for DOOH) and let domain experts refine the details later. Parameters are in the schema for those who need them, but documentation doesn't over-specify. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 165 +++--------------- static/schemas/v1/core/pricing-option.json | 2 +- 2 files changed, 28 insertions(+), 139 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index 1e50d26df7..b08cf867dd 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -45,7 +45,7 @@ Publishers specify their measurement provider in the product: **Common Measurement Providers:** - **Ad Servers**: Google Ad Manager, Freewheel, SpringServe - **Attention Metrics**: Adelaide, Lumen, TVision -- **Third-Party Verification**: IAS, DoubleVerify, MOAT +- **Third-Party Verification**: IAS, DoubleVerify, Scope3 - **TV/Audio Measurement**: Nielsen, Comscore, iSpot.tv, Triton Digital - **DOOH**: Geopath, Vistar, Place Exchange @@ -96,14 +96,11 @@ By accepting the product, buyers agree to use the declared measurement provider "pricing_model": "cpcv", "rate": 0.15, "currency": "USD", - "is_fixed": true, - "parameters": { - "view_threshold": 1.0 - } + "is_fixed": true } ``` -**Billing**: Charged only when viewer completes 100% of the video/audio ad +**Billing**: Charged only when viewer completes 100% of the video/audio ad. Completion is measured by the declared measurement provider. --- @@ -233,150 +230,42 @@ Buyers should verify the measurement provider meets their campaign requirements ## Digital Out-of-Home (DOOH) Pricing -DOOH advertising uses existing pricing models (CPM, flat_rate) enhanced with DOOH-specific parameters. Common buying modes like Share of Voice (SOV), time-based buying, and takeovers are **inventory allocation mechanisms**, not separate pricing models. - -### Key Concept: Inventory Allocation vs. Pricing - -- **Share of Voice**: Percentage of loop plays → Priced as CPM or flat_rate with `sov_percentage` parameter -- **Time-based**: Hourly/daily/daypart booking → Priced as flat_rate with `duration_hours` parameter -- **Takeover**: 100% exclusive placement → Priced as flat_rate with `sov_percentage: 100` - -### DOOH Use Case: Times Square Takeover - -**24-hour exclusive takeover of premium billboard:** - -```json -{ - "product_id": "times_square_digital_takeover", - "name": "Times Square Digital Takeover", - "description": "Exclusive 24-hour takeover of premium Times Square digital billboard", - "format_ids": ["dooh_portrait_1920x1080"], - "delivery_type": "guaranteed", - "pricing_options": [ - { - "pricing_model": "flat_rate", - "rate": 50000.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "duration_hours": 24, - "sov_percentage": 100, - "loop_duration_seconds": 300, - "min_plays_per_hour": 12, - "venue_package": "times_square_premium" - } - } - ] -} -``` - -**Buyer selection:** -```json -{ - "packages": [{ - "buyer_ref": "pkg_launch_takeover", - "products": ["times_square_digital_takeover"], - "format_ids": ["dooh_portrait_1920x1080"], - "budget": 50000, - "pricing_model": "flat_rate" - }] -} -``` - -### DOOH Use Case: Airport SOV Package - -**15% share of voice during morning commute with dual pricing options:** - -```json -{ - "product_id": "airport_morning_commute", - "name": "Airport Morning Commute Package", - "description": "15% share of voice during morning rush (5am-9am)", - "format_ids": ["dooh_portrait_1080x1920"], - "delivery_type": "guaranteed", - "pricing_options": [ - { - "pricing_model": "cpm", - "rate": 25.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "sov_percentage": 15, - "loop_duration_seconds": 180, - "daypart": "morning_commute", - "estimated_impressions": 75000 - }, - "min_spend": 1875 - }, - { - "pricing_model": "flat_rate", - "rate": 2000.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "duration_hours": 4, - "sov_percentage": 15, - "loop_duration_seconds": 180, - "daypart": "morning_commute" - } - } - ] -} -``` +DOOH advertising uses existing pricing models—typically **CPM** or **flat_rate**—with optional parameters to describe the inventory allocation. -**Notice:** Same inventory, two pricing options. Buyers choose based on whether they prefer impression-based (CPM) or time-based (flat_rate) pricing. +### Basic Concepts -### DOOH Use Case: Transit Network Hourly +- **CPM for DOOH**: Priced per thousand impressions, where impressions are calculated based on venue traffic (e.g., Geopath data) +- **Flat rate for DOOH**: Fixed cost for specific duration or allocation (hourly, daily, or exclusive takeover) -**Hourly pricing for retail transit locations:** +### Simple Example: Billboard Takeover ```json { - "product_id": "retail_transit_hourly", - "name": "Retail Transit Network - Hourly", - "description": "Hourly booking across 50 retail transit locations", - "format_ids": ["dooh_landscape_1920x1080"], - "delivery_type": "guaranteed", - "pricing_options": [ - { - "pricing_model": "flat_rate", - "rate": 500.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "duration_hours": 1, - "venue_package": "retail_transit_50_screens", - "loop_duration_seconds": 240, - "min_plays_per_hour": 15 - } - } - ] + "product_id": "billboard_takeover", + "name": "Premium Billboard - 24 Hour Takeover", + "pricing_options": [{ + "pricing_model": "flat_rate", + "rate": 50000.00, + "currency": "USD", + "is_fixed": true + }], + "delivery_measurement": { + "provider": "Geopath", + "notes": "Venue traffic data updated monthly. Estimated 2.5M impressions over 24 hours." + } } ``` -### DOOH Parameters Reference - -| Parameter | Type | Description | Use With | -|-----------|------|-------------|----------| -| `duration_hours` | number | Duration for time-based pricing | flat_rate | -| `sov_percentage` | number | Share of voice (0-100) | cpm, flat_rate | -| `loop_duration_seconds` | integer | Ad loop rotation duration | cpm, flat_rate | -| `min_plays_per_hour` | integer | Minimum frequency guarantee | cpm, flat_rate | -| `venue_package` | string | Named venue package ID | any | -| `estimated_impressions` | integer | Expected impressions | cpm, flat_rate | -| `daypart` | string | Specific daypart targeting | any | - -### DOOH Delivery Metrics +### DOOH Parameters (Optional) -When reporting on DOOH campaigns, publishers should include DOOH-specific metrics: +Publishers may include additional parameters to describe DOOH inventory allocation: -- `loop_plays`: Number of times ad played in rotation -- `screens_used`: Number of unique screens displaying the ad -- `screen_time_seconds`: Total display time across all screens -- `sov_achieved`: Actual share of voice delivered (0.0-1.0) -- `venue_breakdown`: Per-venue performance data +- `duration_hours`: Duration for time-based pricing +- `sov_percentage`: Share of voice (% of available plays) +- `daypart`: Specific time periods (e.g., "morning_commute") +- `venue_package`: Named collection of screens -See [Delivery Reporting](../task-reference/get_media_buy_delivery.md) for full metric definitions. +**Note**: DOOH measurement and buying practices vary by market. Publishers should clearly explain their measurement methodology and inventory allocation in the product description and `delivery_measurement` field. --- diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 1af4a8acf9..570a6f3f8f 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -121,7 +121,7 @@ }, "min_spend": { "type": "number", - "description": "Minimum spend requirement for this pricing option in the specified currency", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", "minimum": 0 } }, From af5c4566002dfb3e5c680dc9730c249e2d05874e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 14:29:23 -0400 Subject: [PATCH 06/16] refactor: rename min_spend to min_spend_per_package for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field name was ambiguous about scope. Renaming makes it explicit that this minimum applies per package, not per media buy. ## Changes - Rename `min_spend` to `min_spend_per_package` in pricing-option.json - Update all documentation examples - Update deprecated field reference in product.json ## Rationale User feedback: "min_spend really needs to be clear - per package? per media buy?" The name should be self-documenting. `min_spend_per_package` is unambiguous. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/media-buy/advanced-topics/pricing-models.md | 12 ++++++------ static/schemas/v1/core/pricing-option.json | 2 +- static/schemas/v1/core/product.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index b08cf867dd..5fbd53828a 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -77,7 +77,7 @@ By accepting the product, buyers agree to use the declared measurement provider "rate": 12.50, "currency": "USD", "is_fixed": true, - "min_spend": 5000 + "min_spend_per_package": 5000 } ``` @@ -145,7 +145,7 @@ By accepting the product, buyers agree to use the declared measurement provider "demographic": "A18-49", "min_points": 50 }, - "min_spend": 12500 + "min_spend_per_package": 12500 } ``` @@ -395,14 +395,14 @@ A publisher offering Connected TV inventory with multiple pricing options: "rate": 55.00, "currency": "USD", "is_fixed": true, - "min_spend": 15000 + "min_spend_per_package": 15000 }, { "pricing_model": "cpcv", "rate": 0.22, "currency": "USD", "is_fixed": true, - "min_spend": 15000 + "min_spend_per_package": 15000 }, { "pricing_model": "cpp", @@ -413,7 +413,7 @@ A publisher offering Connected TV inventory with multiple pricing options: "demographic": "M18-49", "min_points": 50 }, - "min_spend": 15000 + "min_spend_per_package": 15000 } ] } @@ -426,7 +426,7 @@ A buyer could choose CPP pricing if they're planning TV buys, CPCV if optimizing ### For Publishers 1. **Offer relevant pricing models** - Match pricing to your inventory type and buyer expectations -2. **Set appropriate minimums** - Use `min_spend` to ensure campaign viability +2. **Set appropriate minimums** - Use `min_spend_per_package` to ensure campaign viability 3. **Provide price guidance** - For auction pricing, give realistic floor and percentile data 4. **Consider multi-currency** - Support currencies of your target markets 5. **Document parameters** - Clearly explain thresholds, demographics, and action types diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 570a6f3f8f..49d88ceed1 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -119,7 +119,7 @@ } } }, - "min_spend": { + "min_spend_per_package": { "type": "number", "description": "Minimum spend requirement per package using this pricing option, in the specified currency", "minimum": 0 diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index 46b8b46cd0..8472fe6e31 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -71,7 +71,7 @@ }, "min_spend": { "type": "number", - "description": "DEPRECATED: Use pricing_options[].min_spend instead. Minimum budget requirement", + "description": "DEPRECATED: Use pricing_options[].min_spend_per_package instead. Minimum budget requirement", "minimum": 0 }, "estimated_exposures": { From c88485646ac54ac47667adc356d567c117c90b26 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 14:38:50 -0400 Subject: [PATCH 07/16] refactor: extract delivery metrics into shared schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address feedback to avoid duplication across product, package, and potentially creative level reporting. ## Changes 1. **Create delivery-metrics.json schema** - Standard metrics: impressions, spend, clicks, views, etc. - Pricing-specific: grps, reach, frequency, quartile_data - DOOH-specific: dooh_metrics with loop_plays, sov_achieved, etc. - Reusable at any reporting level 2. **Update get-media-buy-delivery-response.json** - Use $ref to delivery-metrics.json with allOf - totals: extends with effective_rate - by_package: extends with package_id, buyer_ref, pacing_index - Eliminates ~200 lines of duplicated schema 3. **Clarify floor description** - "Minimum bid price - publisher will reject bids under this value" - Makes it explicit what floor means in auction context ## Benefits - Single source of truth for metric definitions - Easy to add new metrics in one place - Can be reused for future creative-level reporting - Reduced schema maintenance burden 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/core/delivery-metrics.json | 173 +++++++++++ static/schemas/v1/core/pricing-option.json | 2 +- static/schemas/v1/index.json | 4 + .../get-media-buy-delivery-response.json | 272 ++---------------- 4 files changed, 202 insertions(+), 249 deletions(-) create mode 100644 static/schemas/v1/core/delivery-metrics.json diff --git a/static/schemas/v1/core/delivery-metrics.json b/static/schemas/v1/core/delivery-metrics.json new file mode 100644 index 0000000000..1520e8715a --- /dev/null +++ b/static/schemas/v1/core/delivery-metrics.json @@ -0,0 +1,173 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/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": "Views at threshold (for CPV)", + "minimum": 0 + }, + "completed_views": { + "type": "number", + "description": "100% completions (for CPCV)", + "minimum": 0 + }, + "video_completions": { + "type": "number", + "description": "DEPRECATED: Use completed_views instead", + "minimum": 0 + }, + "completion_rate": { + "type": "number", + "description": "Completion rate (completed_views/impressions)", + "minimum": 0, + "maximum": 1 + }, + "conversions": { + "type": "number", + "description": "Conversions (reserved for future CPA pricing support)", + "minimum": 0 + }, + "leads": { + "type": "number", + "description": "Leads generated (reserved for future CPL pricing support)", + "minimum": 0 + }, + "grps": { + "type": "number", + "description": "Gross Rating Points delivered (for CPP)", + "minimum": 0 + }, + "reach": { + "type": "number", + "description": "Unique individuals reached", + "minimum": 0 + }, + "frequency": { + "type": "number", + "description": "Average frequency per individual", + "minimum": 0 + }, + "quartile_data": { + "type": "object", + "description": "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": "Explanation of how DOOH impressions were calculated" + }, + "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": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true +} diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 49d88ceed1..89362f680b 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -29,7 +29,7 @@ "properties": { "floor": { "type": "number", - "description": "Minimum bid price", + "description": "Minimum bid price - publisher will reject bids under this value", "minimum": 0 }, "p25": { diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index 107d0243f7..4d81289522 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -51,6 +51,10 @@ "$ref": "/schemas/v1/core/measurement.json", "description": "Measurement capabilities included with a product" }, + "delivery-metrics": { + "$ref": "/schemas/v1/core/delivery-metrics.json", + "description": "Standard delivery metrics for reporting" + }, "creative-policy": { "$ref": "/schemas/v1/core/creative-policy.json", "description": "Creative requirements and restrictions for a product" diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json index e63f6af31a..757016ef28 100644 --- a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json @@ -127,276 +127,52 @@ "description": "Pricing model used for this media buy" }, "totals": { - "type": "object", - "description": "Aggregate metrics for this media buy across all packages", - "properties": { - "impressions": { - "type": "number", - "description": "Total impressions delivered", - "minimum": 0 + "allOf": [ + { + "$ref": "/schemas/v1/core/delivery-metrics.json" }, - "spend": { - "type": "number", - "description": "Total amount spent", - "minimum": 0 - }, - "effective_rate": { - "type": "number", - "description": "Effective rate paid per unit based on pricing_model (e.g., actual CPM for 'cpm', actual cost per completed view for 'cpcv', actual cost per point for 'cpp')", - "minimum": 0 - }, - "clicks": { - "type": "number", - "description": "Total clicks (for CPC or general reporting)", - "minimum": 0 - }, - "ctr": { - "type": "number", - "description": "Click-through rate (clicks/impressions)", - "minimum": 0, - "maximum": 1 - }, - "views": { - "type": "number", - "description": "Total views at threshold (for CPV)", - "minimum": 0 - }, - "completed_views": { - "type": "number", - "description": "Total 100% completions (for CPCV)", - "minimum": 0 - }, - "video_completions": { - "type": "number", - "description": "DEPRECATED: Use completed_views instead", - "minimum": 0 - }, - "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", - "minimum": 0, - "maximum": 1 - }, - "conversions": { - "type": "number", - "description": "Total conversions (reserved for future CPA pricing support)", - "minimum": 0 - }, - "leads": { - "type": "number", - "description": "Total leads generated (reserved for future CPL pricing support)", - "minimum": 0 - }, - "grps": { - "type": "number", - "description": "Gross Rating Points delivered (for CPP)", - "minimum": 0 - }, - "reach": { - "type": "number", - "description": "Unique individuals reached", - "minimum": 0 - }, - "frequency": { - "type": "number", - "description": "Average frequency per individual", - "minimum": 0 - }, - "quartile_data": { + { "type": "object", - "description": "Video quartile completion data", + "description": "Aggregate metrics for this media buy across all packages", "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": { + "effective_rate": { "type": "number", - "description": "100% completion views", - "minimum": 0 - } - } - }, - "dooh_metrics": { - "type": "object", - "description": "DOOH-specific delivery metrics (only included for DOOH campaigns)", - "properties": { - "loop_plays": { - "type": "integer", - "description": "Number of times ad played in rotation across all screens", - "minimum": 0 - }, - "screens_used": { - "type": "integer", - "description": "Number of unique screens displaying the ad", + "description": "Effective rate paid per unit based on pricing_model (e.g., actual CPM for 'cpm', actual cost per completed view for 'cpcv', actual cost per point for 'cpp')", "minimum": 0 - }, - "screen_time_seconds": { - "type": "integer", - "description": "Total time ad was displayed across all screens in seconds", - "minimum": 0 - }, - "sov_achieved": { - "type": "number", - "description": "Actual share of voice delivered as decimal (0.0 to 1.0, e.g., 0.15 = 15%)", - "minimum": 0, - "maximum": 1 - }, - "calculation_notes": { - "type": "string", - "description": "Explanation of how DOOH impressions were calculated (e.g., 'Impressions based on Geopath December 2024 venue traffic data. 4,800 loop plays × 15.6 avg impressions per play = 75,000 total impressions.')" - }, - "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": false - } } }, - "additionalProperties": false + "required": ["spend"] } - }, - "required": ["spend"], - "additionalProperties": true + ] }, "by_package": { "type": "array", "description": "Metrics broken down by package", "items": { - "type": "object", - "properties": { - "package_id": { - "type": "string", - "description": "Publisher's package identifier" + "allOf": [ + { + "$ref": "/schemas/v1/core/delivery-metrics.json" }, - "buyer_ref": { - "type": "string", - "description": "Buyer's reference identifier for this package" - }, - "impressions": { - "type": "number", - "description": "Package impressions", - "minimum": 0 - }, - "spend": { - "type": "number", - "description": "Package spend", - "minimum": 0 - }, - "clicks": { - "type": "number", - "description": "Package clicks", - "minimum": 0 - }, - "views": { - "type": "number", - "description": "Package views (for CPV)", - "minimum": 0 - }, - "completed_views": { - "type": "number", - "description": "Package completed views (for CPCV)", - "minimum": 0 - }, - "video_completions": { - "type": "number", - "description": "DEPRECATED: Use completed_views instead", - "minimum": 0 - }, - "conversions": { - "type": "number", - "description": "Package conversions (reserved for future CPA pricing support)", - "minimum": 0 - }, - "leads": { - "type": "number", - "description": "Package leads (reserved for future CPL pricing support)", - "minimum": 0 - }, - "grps": { - "type": "number", - "description": "Package GRPs (for CPP)", - "minimum": 0 - }, - "pacing_index": { - "type": "number", - "description": "Delivery pace (1.0 = on track, <1.0 = behind, >1.0 = ahead)", - "minimum": 0 - }, - "dooh_metrics": { + { "type": "object", - "description": "DOOH-specific metrics for this package (only included for DOOH campaigns)", "properties": { - "loop_plays": { - "type": "integer", - "description": "Loop plays for this package", - "minimum": 0 + "package_id": { + "type": "string", + "description": "Publisher's package identifier" }, - "screens_used": { - "type": "integer", - "description": "Screens used for this package", - "minimum": 0 - }, - "screen_time_seconds": { - "type": "integer", - "description": "Screen time for this package in seconds", - "minimum": 0 + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this package" }, - "sov_achieved": { + "pacing_index": { "type": "number", - "description": "Actual SOV for this package (0.0 to 1.0)", - "minimum": 0, - "maximum": 1 + "description": "Delivery pace (1.0 = on track, <1.0 = behind, >1.0 = ahead)", + "minimum": 0 } }, - "additionalProperties": false + "required": ["package_id", "spend"] } - }, - "required": ["package_id", "spend"], - "additionalProperties": true + ] } }, "daily_breakdown": { From 07374fa463e145320617b7267bb291c1baa6a4e0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:00:39 -0400 Subject: [PATCH 08/16] fix: resolve pricing model validation and measurement requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes based on ad-tech-protocol-expert assessment: 1. **Add pricing_option_id for unambiguous selection** - Added required pricing_option_id field to pricing-option schema - Replaced pricing_model with pricing_option_id in package-request - Eliminates ambiguity when products have multiple options with same model+currency 2. **Make delivery_measurement required for all products** - Changed delivery_measurement from optional to required in product schema - Requires provider field (notes optional) - Ensures measurement transparency for all pricing models (not just CPP) 3. **Expand CPV threshold to support time-based views** - view_threshold now supports both percentage (0.0-1.0) and duration_seconds - Enables YouTube-style "30 seconds = view" pricing models - Uses oneOf for flexible threshold definitions 4. **Standardize CPP demographic codes** - Added pattern validation for Nielsen-style demographic codes - Format: [PMWAC][0-9]{2}(-[0-9]{2}|\\+) - Examples: P18-49, M25-54, W35+, A18-34, C2-11 5. **Update documentation with complete examples** - All pricing options now show pricing_option_id - All products show required delivery_measurement - Added common measurement provider examples - Updated pricing models guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 8 ++- .../product-discovery/media-products.md | 64 +++++++++++++------ static/schemas/v1/core/pricing-option.json | 34 ++++++++-- static/schemas/v1/core/product.json | 8 ++- .../schemas/v1/media-buy/package-request.json | 20 +++--- 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index 5fbd53828a..914d04d9af 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -12,10 +12,10 @@ AdCP supports multiple pricing models to accommodate different advertising chann ### How It Works -1. **Publishers declare pricing options** in their products via `pricing_options` array +1. **Publishers declare pricing options** in their products via `pricing_options` array (each with unique `pricing_option_id`) 2. **Buyers discover available options** through `get_products` -3. **Buyers select a pricing model** when creating a media buy via `pricing_selection` -4. **Delivery is measured** according to the selected pricing model +3. **Buyers select a specific option** when creating a media buy via `pricing_option_id` +4. **Delivery is measured** according to the declared `delivery_measurement` provider ### Key Benefits @@ -73,6 +73,7 @@ By accepting the product, buyers agree to use the declared measurement provider **Example**: ```json { + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 12.50, "currency": "USD", @@ -93,6 +94,7 @@ By accepting the product, buyers agree to use the declared measurement provider **Example**: ```json { + "pricing_option_id": "cpcv_usd_guaranteed", "pricing_model": "cpcv", "rate": 0.15, "currency": "USD", diff --git a/docs/media-buy/product-discovery/media-products.md b/docs/media-buy/product-discovery/media-products.md index cd13169f2c..9c13dc39fb 100644 --- a/docs/media-buy/product-discovery/media-products.md +++ b/docs/media-buy/product-discovery/media-products.md @@ -49,6 +49,7 @@ Publishers declare which pricing models they support for each product. Buyers se Each pricing option includes: ```json { + "pricing_option_id": "cpcv_usd_guaranteed", "pricing_model": "cpcv", "rate": 0.15, "currency": "USD", @@ -56,13 +57,14 @@ Each pricing option includes: "parameters": { "view_threshold": 1.0 }, - "min_spend": 5000 + "min_spend_per_package": 5000 } ``` For auction-based pricing (`is_fixed: false`), include `price_guidance`: ```json { + "pricing_option_id": "cpm_usd_auction", "pricing_model": "cpm", "currency": "USD", "is_fixed": false, @@ -76,6 +78,25 @@ For auction-based pricing (`is_fixed: false`), include `price_guidance`: } ``` +#### Delivery Measurement (Required) + +All products MUST declare their measurement provider: +```json +{ + "delivery_measurement": { + "provider": "Google Ad Manager with IAS viewability verification", + "notes": "MRC-accredited viewability. 50% in-view for 1s display / 2s video." + } +} +``` + +Common provider examples: +- `"Google Ad Manager with IAS viewability"` +- `"Nielsen DAR for P18-49 demographic measurement"` +- `"Geopath DOOH traffic counts updated monthly"` +- `"Comscore vCE for video completion tracking"` +- `"Self-reported impressions from proprietary ad server"` + ### Measurement Object For products that include measurement (common in retail media): @@ -117,31 +138,38 @@ A server can offer a general catalog, but it can also return: "delivery_type": "guaranteed", "pricing_options": [ { + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 45.00, "currency": "USD", "is_fixed": true, - "min_spend": 10000 + "min_spend_per_package": 10000 }, { + "pricing_option_id": "cpcv_usd_guaranteed", "pricing_model": "cpcv", "rate": 0.18, "currency": "USD", "is_fixed": true, - "min_spend": 10000 + "min_spend_per_package": 10000 }, { + "pricing_option_id": "cpp_usd_p18-49", "pricing_model": "cpp", "rate": 250.00, "currency": "USD", "is_fixed": true, "parameters": { - "demographic": "A18-49", + "demographic": "P18-49", "min_points": 50 }, - "min_spend": 12500 + "min_spend_per_package": 12500 } - ] + ], + "delivery_measurement": { + "provider": "Nielsen DAR for P18-49 demographic measurement", + "notes": "Panel-based measurement for GRP delivery. Impressions measured via Comscore vCE." + } } ``` @@ -155,6 +183,7 @@ A server can offer a general catalog, but it can also return: "delivery_type": "non_guaranteed", "pricing_options": [ { + "pricing_option_id": "cpm_usd_auction", "pricing_model": "cpm", "currency": "USD", "is_fixed": false, @@ -165,6 +194,7 @@ A server can offer a general catalog, but it can also return: } }, { + "pricing_option_id": "cpc_usd_auction", "pricing_model": "cpc", "currency": "USD", "is_fixed": false, @@ -175,6 +205,10 @@ A server can offer a general catalog, but it can also return: } } ], + "delivery_measurement": { + "provider": "Google Ad Manager with IAS viewability", + "notes": "MRC-accredited viewability. 50% in-view for 1s display." + }, "is_custom": true, "expires_at": "2025-02-15T00:00:00Z" } @@ -194,24 +228,18 @@ A server can offer a general catalog, but it can also return: "delivery_type": "guaranteed", "pricing_options": [ { + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 13.50, "currency": "USD", "is_fixed": true, - "min_spend": 10000 - }, - { - "pricing_model": "cpa", - "rate": 15.00, - "currency": "USD", - "is_fixed": true, - "parameters": { - "action_type": "purchase", - "attribution_window_days": 30 - }, - "min_spend": 10000 + "min_spend_per_package": 10000 } ], + "delivery_measurement": { + "provider": "Self-reported impressions from proprietary ad server", + "notes": "Impressions counted per IAB guidelines. Viewability measured via IAS." + }, "measurement": { "type": "incremental_sales_lift", "attribution": "deterministic_purchase", diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 89362f680b..e901c1b072 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -5,6 +5,10 @@ "description": "A pricing model option offered by a publisher for a product", "type": "object", "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_guaranteed', 'cpcv_usd_auction')" + }, "pricing_model": { "$ref": "/schemas/v1/enums/pricing-model.json" }, @@ -61,7 +65,8 @@ "properties": { "demographic": { "type": "string", - "description": "Target demographic for CPP pricing (e.g., 'A18-49', 'W25-54')" + "pattern": "^[PMWAC][0-9]{2}(-[0-9]{2}|\\+)$", + "description": "Target demographic for CPP pricing in Nielsen format: P/M/W/A/C + age range. Examples: P18-49 (Persons 18-49), M25-54 (Men 25-54), W35+ (Women 35+), A18-34 (Adults 18-34), C2-11 (Children 2-11)" }, "min_points": { "type": "number", @@ -69,10 +74,27 @@ "minimum": 0 }, "view_threshold": { - "type": "number", - "description": "Percentage of video/audio that must be viewed for CPV pricing (0.0 to 1.0)", - "minimum": 0, - "maximum": 1 + "oneOf": [ + { + "type": "number", + "description": "Percentage completion threshold for CPV pricing (0.0 to 1.0, e.g., 0.5 = 50% completion)", + "minimum": 0, + "maximum": 1 + }, + { + "type": "object", + "description": "Time-based view threshold for CPV pricing", + "properties": { + "duration_seconds": { + "type": "integer", + "description": "Seconds of viewing required (e.g., 30 for YouTube-style '30 seconds = view')", + "minimum": 1 + } + }, + "required": ["duration_seconds"], + "additionalProperties": false + } + ] }, "action_type": { "type": "string", @@ -125,7 +147,7 @@ "minimum": 0 } }, - "required": ["pricing_model", "currency", "is_fixed"], + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], "allOf": [ { "if": { diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index 8472fe6e31..dea1d73ff2 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -94,7 +94,7 @@ }, "delivery_measurement": { "type": "object", - "description": "Measurement provider and methodology for delivery metrics. The buyer accepts the declared provider as the source of truth for the buy.", + "description": "Measurement provider and methodology for delivery metrics. The buyer accepts the declared provider as the source of truth for the buy. REQUIRED for all products.", "properties": { "provider": { "type": "string", @@ -104,7 +104,8 @@ "type": "string", "description": "Additional details about measurement methodology in plain language (e.g., 'MRC-accredited viewability. 50% in-view for 1s display / 2s video', 'Panel-based demographic measurement updated monthly')" } - } + }, + "required": ["provider"] }, "reporting_capabilities": { "$ref": "/schemas/v1/core/reporting-capabilities.json" @@ -153,7 +154,8 @@ "name", "description", "format_ids", - "delivery_type" + "delivery_type", + "delivery_measurement" ], "additionalProperties": false } \ No newline at end of file diff --git a/static/schemas/v1/media-buy/package-request.json b/static/schemas/v1/media-buy/package-request.json index 619aa00fbf..328897ff95 100644 --- a/static/schemas/v1/media-buy/package-request.json +++ b/static/schemas/v1/media-buy/package-request.json @@ -40,9 +40,9 @@ "pacing": { "$ref": "/schemas/v1/enums/pacing.json" }, - "pricing_model": { - "$ref": "/schemas/v1/enums/pricing-model.json", - "description": "Selected pricing model from the product's available pricing_options (must be available in the media buy's currency)" + "pricing_option_id": { + "type": "string", + "description": "ID of the selected pricing option from the product's pricing_options array" }, "bid_price": { "type": "number", @@ -61,8 +61,8 @@ } }, "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_model"]}, - {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_model"]} + {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_option_id"]}, + {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_option_id"]} ], "not": { "required": ["format_selection"] @@ -101,9 +101,9 @@ "pacing": { "$ref": "/schemas/v1/enums/pacing.json" }, - "pricing_model": { - "$ref": "/schemas/v1/enums/pricing-model.json", - "description": "Selected pricing model from the product's available pricing_options (must be available in the media buy's currency)" + "pricing_option_id": { + "type": "string", + "description": "ID of the selected pricing option from the product's pricing_options array" }, "bid_price": { "type": "number", @@ -122,8 +122,8 @@ } }, "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_selection", "budget", "pricing_model"]}, - {"required": ["buyer_ref", "products", "format_selection", "budget", "pricing_model"]} + {"required": ["buyer_ref", "product_id", "format_selection", "budget", "pricing_option_id"]}, + {"required": ["buyer_ref", "products", "format_selection", "budget", "pricing_option_id"]} ], "not": { "required": ["format_ids"] From f25c86b06a1b5cd7f3311585c9eca4e5fa4dd702 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:13:59 -0400 Subject: [PATCH 09/16] docs: add pricing_option_id to all remaining examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completed the migration from pricing_model to pricing_option_id throughout documentation: - Multi-currency support example (3 pricing options with different currencies) - Auction pricing example - Buyer selection process example (changed from pricing_model to pricing_option_id) - Multi-model CTV product example (3 pricing options) - Migration guide example Updated buyer selection flow documentation to clarify: - Packages now select pricing_option_id (not pricing_model) - The pricing option ID fully specifies model, currency, and fixed/auction - More explicit about when bid_price is required All documentation now consistently uses pricing_option_id for package selection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced-topics/pricing-models.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/media-buy/advanced-topics/pricing-models.md b/docs/media-buy/advanced-topics/pricing-models.md index 914d04d9af..561fb80d07 100644 --- a/docs/media-buy/advanced-topics/pricing-models.md +++ b/docs/media-buy/advanced-topics/pricing-models.md @@ -280,18 +280,21 @@ Publishers can offer the same product in multiple currencies: "product_id": "premium_video", "pricing_options": [ { + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 45.00, "currency": "USD", "is_fixed": true }, { + "pricing_option_id": "cpm_eur_guaranteed", "pricing_model": "cpm", "rate": 40.00, "currency": "EUR", "is_fixed": true }, { + "pricing_option_id": "cpm_gbp_guaranteed", "pricing_model": "cpm", "rate": 35.00, "currency": "GBP", @@ -320,6 +323,7 @@ Publishers can offer the same product in multiple currencies: **Auction Example**: ```json { + "pricing_option_id": "cpcv_usd_auction", "pricing_model": "cpcv", "currency": "USD", "is_fixed": false, @@ -335,7 +339,7 @@ Publishers can offer the same product in multiple currencies: ## Buyer Selection Process -Currency is set at the **media buy level**, packages specify their pricing model and budget allocation: +Currency is set at the **media buy level**, packages specify their pricing option and budget allocation: ```json { @@ -353,7 +357,7 @@ Currency is set at the **media buy level**, packages specify their pricing model "format_ids": ["video_15s", "video_30s"], "budget": 50000, "pacing": "even", - "pricing_model": "cpcv", + "pricing_option_id": "cpcv_usd_auction", "bid_price": 0.16 }] } @@ -361,11 +365,11 @@ Currency is set at the **media buy level**, packages specify their pricing model **How it works:** 1. Media buy sets overall `budget.currency` (e.g., "USD") - applies to all packages -2. Each package selects a `pricing_model` (e.g., "cpcv") -3. Currency + pricing_model identify which pricing option from the product applies +2. Each package selects a specific `pricing_option_id` from the product (e.g., "cpcv_usd_auction") +3. The pricing option ID fully specifies the pricing model, currency, and whether it's fixed or auction 4. Package sets its budget allocation as a number in the media buy's currency 5. Package can specify `pacing` strategy (even, frontload, etc.) -6. If auction-based, package includes `bid_price` +6. If the selected pricing option is auction-based (`is_fixed: false`), package must include `bid_price` ## Reporting Metrics by Pricing Model @@ -393,6 +397,7 @@ A publisher offering Connected TV inventory with multiple pricing options: "delivery_type": "guaranteed", "pricing_options": [ { + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 55.00, "currency": "USD", @@ -400,6 +405,7 @@ A publisher offering Connected TV inventory with multiple pricing options: "min_spend_per_package": 15000 }, { + "pricing_option_id": "cpcv_usd_guaranteed", "pricing_model": "cpcv", "rate": 0.22, "currency": "USD", @@ -407,6 +413,7 @@ A publisher offering Connected TV inventory with multiple pricing options: "min_spend_per_package": 15000 }, { + "pricing_option_id": "cpp_usd_m18-49", "pricing_model": "cpp", "rate": 300.00, "currency": "USD", @@ -459,6 +466,7 @@ For backward compatibility, products can still use the deprecated `is_fixed_pric { "product_id": "display_standard", "pricing_options": [{ + "pricing_option_id": "cpm_usd_guaranteed", "pricing_model": "cpm", "rate": 12.50, "currency": "USD", From 07e9fa97faf4bf2277b689125df6c7a2908eed33 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:20:41 -0400 Subject: [PATCH 10/16] docs: clarify reach/frequency measurement and pricing model parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addressed documentation ambiguities in delivery metrics: 1. **Frequency timeframe**: Added note that frequency is typically measured over campaign duration but varies by measurement provider 2. **Reach measurement units**: Clarified that reach units depend on measurement provider: - TV: households (Nielsen) - Digital: individuals/devices/cookies (various) - Reference delivery_measurement.provider for methodology 3. **Pricing model parameters**: Added comprehensive description of expected parameters per model to pricing-option schema: - CPV: view_threshold - CPP: demographic, min_points - DOOH/flat_rate: duration_hours, sov_percentage, venue_package, daypart These clarifications maintain the flexible schema approach while improving developer understanding of what's expected for each pricing model. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/core/delivery-metrics.json | 4 ++-- static/schemas/v1/core/pricing-option.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/schemas/v1/core/delivery-metrics.json b/static/schemas/v1/core/delivery-metrics.json index 1520e8715a..cd168b2289 100644 --- a/static/schemas/v1/core/delivery-metrics.json +++ b/static/schemas/v1/core/delivery-metrics.json @@ -64,12 +64,12 @@ }, "reach": { "type": "number", - "description": "Unique individuals reached", + "description": "Unique reach - units depend on measurement provider (e.g., individuals, households, devices, cookies). See delivery_measurement.provider for methodology.", "minimum": 0 }, "frequency": { "type": "number", - "description": "Average frequency per individual", + "description": "Average frequency per individual (typically measured over campaign duration, but can vary by measurement provider)", "minimum": 0 }, "quartile_data": { diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index e901c1b072..797eed04cb 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -61,7 +61,7 @@ }, "parameters": { "type": "object", - "description": "Additional parameters specific to this pricing model", + "description": "Additional parameters specific to this pricing model. Expected parameters by model: CPV (view_threshold), CPP (demographic, min_points), DOOH/flat_rate (duration_hours, sov_percentage, venue_package, daypart). See pricing models documentation for full details.", "properties": { "demographic": { "type": "string", From b5e7e41bb62e8de329871028c4e6c5feac890bb1 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:21:50 -0400 Subject: [PATCH 11/16] refactor: remove format_selection, require format_ids for all packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Removed dynamic format_selection option from package-request schema Simplified package-request to always require explicit format_ids: - Removed the oneOf with two package variants - format_selection field no longer supported - All packages must specify format_ids array (minItems: 1) - Maintains support for both product_id and deprecated products array This removes technical debt from the undefined format_selection structure and aligns with the brief-first approach where buyers explicitly specify which formats they want to provide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../schemas/v1/media-buy/package-request.json | 182 ++++++------------ 1 file changed, 57 insertions(+), 125 deletions(-) diff --git a/static/schemas/v1/media-buy/package-request.json b/static/schemas/v1/media-buy/package-request.json index 328897ff95..27260592c3 100644 --- a/static/schemas/v1/media-buy/package-request.json +++ b/static/schemas/v1/media-buy/package-request.json @@ -3,132 +3,64 @@ "$id": "/schemas/v1/media-buy/package-request.json", "title": "Package Request", "description": "Package configuration for media buy creation", - "oneOf": [ - { - "type": "object", - "description": "Package with explicit format IDs", - "properties": { - "buyer_ref": { - "type": "string", - "description": "Buyer's reference identifier for this package" - }, - "product_id": { - "type": "string", - "description": "Product ID for this package (recommended - replaces deprecated products array)" - }, - "products": { - "type": "array", - "description": "DEPRECATED: Use product_id instead. Array of product IDs - only first product will be used", - "deprecated": true, - "items": { - "type": "string" - } - }, - "format_ids": { - "type": "array", - "description": "Array of format IDs that will be used for this package - must be supported by the product", - "items": { - "type": "string", - "description": "Format ID referencing a format from list_creative_formats" - } - }, - "budget": { - "type": "number", - "description": "Budget allocation for this package in the media buy's currency", - "minimum": 0 - }, - "pacing": { - "$ref": "/schemas/v1/enums/pacing.json" - }, - "pricing_option_id": { - "type": "string", - "description": "ID of the selected pricing option from the product's pricing_options array" - }, - "bid_price": { - "type": "number", - "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", - "minimum": 0 - }, - "targeting_overlay": { - "$ref": "/schemas/v1/core/targeting.json" - }, - "creative_ids": { - "type": "array", - "description": "Creative IDs to assign to this package at creation time", - "items": { - "type": "string" - } - } - }, - "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_option_id"]}, - {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_option_id"]} - ], - "not": { - "required": ["format_selection"] - }, - "additionalProperties": false + "type": "object", + "properties": { + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this package" }, - { - "type": "object", - "description": "Package with dynamic format selection", - "properties": { - "buyer_ref": { - "type": "string", - "description": "Buyer's reference identifier for this package" - }, - "product_id": { - "type": "string", - "description": "Product ID for this package (recommended - replaces deprecated products array)" - }, - "products": { - "type": "array", - "description": "DEPRECATED: Use product_id instead. Array of product IDs - only first product will be used", - "deprecated": true, - "items": { - "type": "string" - } - }, - "format_selection": { - "type": "object", - "description": "Dynamic format selection criteria" - }, - "budget": { - "type": "number", - "description": "Budget allocation for this package in the media buy's currency", - "minimum": 0 - }, - "pacing": { - "$ref": "/schemas/v1/enums/pacing.json" - }, - "pricing_option_id": { - "type": "string", - "description": "ID of the selected pricing option from the product's pricing_options array" - }, - "bid_price": { - "type": "number", - "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", - "minimum": 0 - }, - "targeting_overlay": { - "$ref": "/schemas/v1/core/targeting.json" - }, - "creative_ids": { - "type": "array", - "description": "Creative IDs to assign to this package at creation time", - "items": { - "type": "string" - } - } - }, - "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_selection", "budget", "pricing_option_id"]}, - {"required": ["buyer_ref", "products", "format_selection", "budget", "pricing_option_id"]} - ], - "not": { - "required": ["format_ids"] + "product_id": { + "type": "string", + "description": "Product ID for this package (recommended - replaces deprecated products array)" + }, + "products": { + "type": "array", + "description": "DEPRECATED: Use product_id instead. Array of product IDs - only first product will be used", + "deprecated": true, + "items": { + "type": "string" + } + }, + "format_ids": { + "type": "array", + "description": "Array of format IDs that will be used for this package - must be supported by the product", + "items": { + "type": "string", + "description": "Format ID referencing a format from list_creative_formats" }, - "additionalProperties": false + "minItems": 1 + }, + "budget": { + "type": "number", + "description": "Budget allocation for this package in the media buy's currency", + "minimum": 0 + }, + "pacing": { + "$ref": "/schemas/v1/enums/pacing.json" + }, + "pricing_option_id": { + "type": "string", + "description": "ID of the selected pricing option from the product's pricing_options array" + }, + "bid_price": { + "type": "number", + "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", + "minimum": 0 + }, + "targeting_overlay": { + "$ref": "/schemas/v1/core/targeting.json" + }, + "creative_ids": { + "type": "array", + "description": "Creative IDs to assign to this package at creation time", + "items": { + "type": "string" + } } - ] + }, + "anyOf": [ + {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_option_id"]}, + {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_option_id"]} + ], + "additionalProperties": false } From 1565aff0fd2db91d8e44386a5e8a85d251a1f8f0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:24:55 -0400 Subject: [PATCH 12/16] refactor: separate pricing option schemas per model for maintainability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Restructured pricing-option.json into discriminated union with separate schemas per pricing model Created dedicated schemas for each pricing model: - cpm-option.json: Display advertising (no parameters) - cpc-option.json: Click-based pricing (no parameters) - cpcv-option.json: Video completion pricing (no parameters) - cpv-option.json: View threshold pricing (requires view_threshold parameter) - cpp-option.json: GRP-based pricing (requires demographic parameter) - flat-rate-option.json: DOOH/sponsorships (optional DOOH parameters) Benefits: 1. **Isolated maintenance**: DOOH experts can modify flat-rate-option.json without risk of breaking CPM 2. **Clear requirements**: Each schema explicitly declares required parameters 3. **Better validation**: JSON Schema enforces model-specific constraints (e.g., CPP requires demographic) 4. **Documentation**: Each file is self-contained with full context for that pricing model 5. **Discoverability**: New pricing models can be added as separate files The main pricing-option.json now uses oneOf to reference all model-specific schemas. Schema registry updated with new pricing-options section. This addresses the concern: "If digital out of home is totally different, it's actually easier to maintain if each pricing method is completely distinct." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/core/pricing-option.json | 170 ++---------------- static/schemas/v1/index.json | 29 +++ .../v1/pricing-options/cpc-option.json | 90 ++++++++++ .../v1/pricing-options/cpcv-option.json | 90 ++++++++++ .../v1/pricing-options/cpm-option.json | 90 ++++++++++ .../v1/pricing-options/cpp-option.json | 108 +++++++++++ .../v1/pricing-options/cpv-option.json | 121 +++++++++++++ .../v1/pricing-options/flat-rate-option.json | 82 +++++++++ 8 files changed, 623 insertions(+), 157 deletions(-) create mode 100644 static/schemas/v1/pricing-options/cpc-option.json create mode 100644 static/schemas/v1/pricing-options/cpcv-option.json create mode 100644 static/schemas/v1/pricing-options/cpm-option.json create mode 100644 static/schemas/v1/pricing-options/cpp-option.json create mode 100644 static/schemas/v1/pricing-options/cpv-option.json create mode 100644 static/schemas/v1/pricing-options/flat-rate-option.json diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index 797eed04cb..b11f9c897f 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -2,169 +2,25 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/core/pricing-option.json", "title": "Pricing Option", - "description": "A pricing model option offered by a publisher for a product", - "type": "object", - "properties": { - "pricing_option_id": { - "type": "string", - "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_guaranteed', 'cpcv_usd_auction')" - }, - "pricing_model": { - "$ref": "/schemas/v1/enums/pricing-model.json" - }, - "rate": { - "type": "number", - "description": "The rate for this pricing model (interpretation depends on pricing_model type)", - "minimum": 0 - }, - "currency": { - "type": "string", - "description": "ISO 4217 currency code", - "pattern": "^[A-Z]{3}$", - "examples": ["USD", "EUR", "GBP", "JPY"] + "description": "A pricing model option offered by a publisher for a product. Each pricing model has its own schema with model-specific requirements.", + "oneOf": [ + { + "$ref": "/schemas/v1/pricing-options/cpm-option.json" }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" + { + "$ref": "/schemas/v1/pricing-options/cpc-option.json" }, - "price_guidance": { - "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", - "properties": { - "floor": { - "type": "number", - "description": "Minimum bid price - publisher will reject bids under this value", - "minimum": 0 - }, - "p25": { - "type": "number", - "description": "25th percentile winning price", - "minimum": 0 - }, - "p50": { - "type": "number", - "description": "Median winning price", - "minimum": 0 - }, - "p75": { - "type": "number", - "description": "75th percentile winning price", - "minimum": 0 - }, - "p90": { - "type": "number", - "description": "90th percentile winning price", - "minimum": 0 - } - }, - "required": ["floor"] + { + "$ref": "/schemas/v1/pricing-options/cpcv-option.json" }, - "parameters": { - "type": "object", - "description": "Additional parameters specific to this pricing model. Expected parameters by model: CPV (view_threshold), CPP (demographic, min_points), DOOH/flat_rate (duration_hours, sov_percentage, venue_package, daypart). See pricing models documentation for full details.", - "properties": { - "demographic": { - "type": "string", - "pattern": "^[PMWAC][0-9]{2}(-[0-9]{2}|\\+)$", - "description": "Target demographic for CPP pricing in Nielsen format: P/M/W/A/C + age range. Examples: P18-49 (Persons 18-49), M25-54 (Men 25-54), W35+ (Women 35+), A18-34 (Adults 18-34), C2-11 (Children 2-11)" - }, - "min_points": { - "type": "number", - "description": "Minimum GRPs/TRPs required for CPP pricing", - "minimum": 0 - }, - "view_threshold": { - "oneOf": [ - { - "type": "number", - "description": "Percentage completion threshold for CPV pricing (0.0 to 1.0, e.g., 0.5 = 50% completion)", - "minimum": 0, - "maximum": 1 - }, - { - "type": "object", - "description": "Time-based view threshold for CPV pricing", - "properties": { - "duration_seconds": { - "type": "integer", - "description": "Seconds of viewing required (e.g., 30 for YouTube-style '30 seconds = view')", - "minimum": 1 - } - }, - "required": ["duration_seconds"], - "additionalProperties": false - } - ] - }, - "action_type": { - "type": "string", - "description": "Type of action for CPA pricing (e.g., 'purchase', 'sign_up', 'download')" - }, - "attribution_window_days": { - "type": "integer", - "description": "Attribution window in days for CPA/CPL pricing", - "minimum": 1 - }, - "duration_hours": { - "type": "number", - "description": "Duration in hours for time-based flat rate pricing (DOOH)", - "minimum": 0 - }, - "sov_percentage": { - "type": "number", - "description": "Guaranteed share of voice as percentage (DOOH, 0-100)", - "minimum": 0, - "maximum": 100 - }, - "loop_duration_seconds": { - "type": "integer", - "description": "Duration of ad loop rotation in seconds (DOOH)", - "minimum": 1 - }, - "min_plays_per_hour": { - "type": "integer", - "description": "Minimum number of times ad plays per hour (DOOH frequency guarantee)", - "minimum": 0 - }, - "venue_package": { - "type": "string", - "description": "Named venue package identifier for DOOH (e.g., 'times_square_network', 'airport_terminals')" - }, - "estimated_impressions": { - "type": "integer", - "description": "Estimated impressions for this pricing option (informational, commonly used with SOV or time-based DOOH)", - "minimum": 0 - }, - "daypart": { - "type": "string", - "description": "Specific daypart for time-based pricing (e.g., 'morning_commute', 'evening_prime', 'overnight')" - } - } + { + "$ref": "/schemas/v1/pricing-options/cpv-option.json" }, - "min_spend_per_package": { - "type": "number", - "description": "Minimum spend requirement per package using this pricing option, in the specified currency", - "minimum": 0 - } - }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], - "allOf": [ { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } + "$ref": "/schemas/v1/pricing-options/cpp-option.json" }, { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } + "$ref": "/schemas/v1/pricing-options/flat-rate-option.json" } - ], - "additionalProperties": false + ] } diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index 4d81289522..ad23660688 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -158,6 +158,35 @@ } } }, + "pricing-options": { + "description": "Individual pricing model schemas with model-specific validation", + "schemas": { + "cpm-option": { + "$ref": "/schemas/v1/pricing-options/cpm-option.json", + "description": "Cost Per Mille (CPM) pricing option for display advertising" + }, + "cpc-option": { + "$ref": "/schemas/v1/pricing-options/cpc-option.json", + "description": "Cost Per Click (CPC) pricing option for performance campaigns" + }, + "cpcv-option": { + "$ref": "/schemas/v1/pricing-options/cpcv-option.json", + "description": "Cost Per Completed View (CPCV) pricing option for video/audio" + }, + "cpv-option": { + "$ref": "/schemas/v1/pricing-options/cpv-option.json", + "description": "Cost Per View (CPV) pricing option with threshold" + }, + "cpp-option": { + "$ref": "/schemas/v1/pricing-options/cpp-option.json", + "description": "Cost Per Point (CPP) pricing option for TV/audio with demographic measurement" + }, + "flat-rate-option": { + "$ref": "/schemas/v1/pricing-options/flat-rate-option.json", + "description": "Flat rate pricing option for DOOH and sponsorships" + } + } + }, "media-buy": { "description": "Media buy task request/response schemas", "supporting-schemas": { diff --git a/static/schemas/v1/pricing-options/cpc-option.json b/static/schemas/v1/pricing-options/cpc-option.json new file mode 100644 index 0000000000..97ffa8f87a --- /dev/null +++ b/static/schemas/v1/pricing-options/cpc-option.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpc-option.json", + "title": "CPC Pricing Option", + "description": "Cost Per Click pricing for performance-driven advertising campaigns", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpc_usd_auction')" + }, + "pricing_model": { + "type": "string", + "const": "cpc", + "description": "Cost per click" + }, + "rate": { + "type": "number", + "description": "CPC rate (cost per click)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price - publisher will reject bids under this value", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/cpcv-option.json b/static/schemas/v1/pricing-options/cpcv-option.json new file mode 100644 index 0000000000..2527ebb4a5 --- /dev/null +++ b/static/schemas/v1/pricing-options/cpcv-option.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpcv-option.json", + "title": "CPCV Pricing Option", + "description": "Cost Per Completed View (100% video/audio completion) pricing", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpcv_usd_guaranteed')" + }, + "pricing_model": { + "type": "string", + "const": "cpcv", + "description": "Cost per completed view (100% completion)" + }, + "rate": { + "type": "number", + "description": "CPCV rate (cost per 100% completion)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price - publisher will reject bids under this value", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/cpm-option.json b/static/schemas/v1/pricing-options/cpm-option.json new file mode 100644 index 0000000000..c04122cf5a --- /dev/null +++ b/static/schemas/v1/pricing-options/cpm-option.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpm-option.json", + "title": "CPM Pricing Option", + "description": "Cost Per Mille (cost per 1,000 impressions) pricing for display and banner advertising", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_guaranteed')" + }, + "pricing_model": { + "type": "string", + "const": "cpm", + "description": "Cost per 1,000 impressions" + }, + "rate": { + "type": "number", + "description": "CPM rate (cost per 1,000 impressions)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price - publisher will reject bids under this value", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/cpp-option.json b/static/schemas/v1/pricing-options/cpp-option.json new file mode 100644 index 0000000000..9cfc6c113e --- /dev/null +++ b/static/schemas/v1/pricing-options/cpp-option.json @@ -0,0 +1,108 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpp-option.json", + "title": "CPP Pricing Option", + "description": "Cost Per Point (Gross Rating Point) pricing for TV and audio campaigns requiring demographic measurement", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpp_usd_p18-49')" + }, + "pricing_model": { + "type": "string", + "const": "cpp", + "description": "Cost per Gross Rating Point" + }, + "rate": { + "type": "number", + "description": "CPP rate (cost per rating point)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price - publisher will reject bids under this value", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "parameters": { + "type": "object", + "description": "CPP-specific parameters for demographic targeting and GRP requirements", + "properties": { + "demographic": { + "type": "string", + "pattern": "^[PMWAC][0-9]{2}(-[0-9]{2}|\\+)$", + "description": "Target demographic in Nielsen format: P/M/W/A/C + age range. Examples: P18-49 (Persons 18-49), M25-54 (Men 25-54), W35+ (Women 35+), A18-34 (Adults 18-34), C2-11 (Children 2-11)" + }, + "min_points": { + "type": "number", + "description": "Minimum GRPs/TRPs required for this pricing option", + "minimum": 0 + } + }, + "required": ["demographic"], + "additionalProperties": false + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed", "parameters"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/cpv-option.json b/static/schemas/v1/pricing-options/cpv-option.json new file mode 100644 index 0000000000..dd9a91ef76 --- /dev/null +++ b/static/schemas/v1/pricing-options/cpv-option.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpv-option.json", + "title": "CPV Pricing Option", + "description": "Cost Per View (at publisher-defined threshold) pricing for video/audio", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpv_usd_50pct')" + }, + "pricing_model": { + "type": "string", + "const": "cpv", + "description": "Cost per view at threshold" + }, + "rate": { + "type": "number", + "description": "CPV rate (cost per view)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)" + }, + "price_guidance": { + "type": "object", + "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "properties": { + "floor": { + "type": "number", + "description": "Minimum bid price - publisher will reject bids under this value", + "minimum": 0 + }, + "p25": { + "type": "number", + "description": "25th percentile winning price", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median winning price", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile winning price", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile winning price", + "minimum": 0 + } + }, + "required": ["floor"] + }, + "parameters": { + "type": "object", + "description": "CPV-specific parameters defining the view threshold", + "properties": { + "view_threshold": { + "oneOf": [ + { + "type": "number", + "description": "Percentage completion threshold for CPV pricing (0.0 to 1.0, e.g., 0.5 = 50% completion)", + "minimum": 0, + "maximum": 1 + }, + { + "type": "object", + "description": "Time-based view threshold for CPV pricing", + "properties": { + "duration_seconds": { + "type": "integer", + "description": "Seconds of viewing required (e.g., 30 for YouTube-style '30 seconds = view')", + "minimum": 1 + } + }, + "required": ["duration_seconds"], + "additionalProperties": false + } + ] + } + }, + "required": ["view_threshold"], + "additionalProperties": false + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed", "parameters"], + "allOf": [ + { + "if": { + "properties": { "is_fixed": { "const": true } } + }, + "then": { + "required": ["rate"] + } + }, + { + "if": { + "properties": { "is_fixed": { "const": false } } + }, + "then": { + "required": ["price_guidance"] + } + } + ], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/flat-rate-option.json b/static/schemas/v1/pricing-options/flat-rate-option.json new file mode 100644 index 0000000000..bb14f9f5be --- /dev/null +++ b/static/schemas/v1/pricing-options/flat-rate-option.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/flat-rate-option.json", + "title": "Flat Rate Pricing Option", + "description": "Flat rate pricing for DOOH, sponsorships, and time-based campaigns - fixed cost regardless of delivery volume", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'flat_rate_usd_24h_takeover')" + }, + "pricing_model": { + "type": "string", + "const": "flat_rate", + "description": "Fixed cost regardless of delivery volume" + }, + "rate": { + "type": "number", + "description": "Flat rate cost", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "is_fixed": { + "type": "boolean", + "description": "Whether this is a fixed rate (true) or auction-based (false)", + "const": true + }, + "parameters": { + "type": "object", + "description": "Flat rate parameters for DOOH and time-based campaigns", + "properties": { + "duration_hours": { + "type": "number", + "description": "Duration in hours for time-based flat rate pricing (DOOH)", + "minimum": 0 + }, + "sov_percentage": { + "type": "number", + "description": "Guaranteed share of voice as percentage (DOOH, 0-100)", + "minimum": 0, + "maximum": 100 + }, + "loop_duration_seconds": { + "type": "integer", + "description": "Duration of ad loop rotation in seconds (DOOH)", + "minimum": 1 + }, + "min_plays_per_hour": { + "type": "integer", + "description": "Minimum number of times ad plays per hour (DOOH frequency guarantee)", + "minimum": 0 + }, + "venue_package": { + "type": "string", + "description": "Named venue package identifier for DOOH (e.g., 'times_square_network', 'airport_terminals')" + }, + "estimated_impressions": { + "type": "integer", + "description": "Estimated impressions for this flat rate option (informational, commonly used with SOV or time-based DOOH)", + "minimum": 0 + }, + "daypart": { + "type": "string", + "description": "Specific daypart for time-based pricing (e.g., 'morning_commute', 'evening_prime', 'overnight')" + } + }, + "additionalProperties": false + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed", "rate"], + "additionalProperties": false +} From 256027c49e92035d886d2c63dcc3d60b8a21f268 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:42:52 -0400 Subject: [PATCH 13/16] refactor: split CPM fixed vs auction, remove auction support from other models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Only CPM supports auction pricing; all other models are fixed-rate only Changes: 1. **CPM split into two schemas:** - cpm-fixed-option.json: Requires `rate`, no `is_fixed` field - cpm-auction-option.json: Requires `price_guidance`, no `rate` field 2. **Non-CPM schemas simplified (fixed-rate only):** - cpc-option.json: Removed `is_fixed`, `price_guidance`, `allOf` validation - cpcv-option.json: Removed `is_fixed`, `price_guidance`, `allOf` validation - cpv-option.json: Removed `is_fixed`, `price_guidance`, `allOf` validation - cpp-option.json: Removed `is_fixed`, `price_guidance`, `allOf` validation - flat-rate-option.json: Already fixed-rate only 3. **Benefits:** - No conditional validation needed (no more `allOf` if/then logic) - Schemas accurately reflect business reality: only CPM has auction markets - Clearer validation errors: "rate is required" vs "rate or price_guidance depending on is_fixed" - Impossible to create invalid combinations (e.g., CPC with price_guidance) The pricing-option.json union now references 7 schemas instead of 6: - cpm-fixed-option - cpm-auction-option - cpc-option (fixed) - cpcv-option (fixed) - cpv-option (fixed) - cpp-option (fixed) - flat-rate-option (fixed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/core/pricing-option.json | 5 +- static/schemas/v1/index.json | 22 ++++--- .../v1/pricing-options/cpc-option.json | 62 ++----------------- .../v1/pricing-options/cpcv-option.json | 60 +----------------- ...pm-option.json => cpm-auction-option.json} | 39 ++---------- .../v1/pricing-options/cpm-fixed-option.json | 36 +++++++++++ .../v1/pricing-options/cpp-option.json | 60 +----------------- .../v1/pricing-options/cpv-option.json | 60 +----------------- 8 files changed, 72 insertions(+), 272 deletions(-) rename static/schemas/v1/pricing-options/{cpm-option.json => cpm-auction-option.json} (61%) create mode 100644 static/schemas/v1/pricing-options/cpm-fixed-option.json diff --git a/static/schemas/v1/core/pricing-option.json b/static/schemas/v1/core/pricing-option.json index b11f9c897f..7e88efc154 100644 --- a/static/schemas/v1/core/pricing-option.json +++ b/static/schemas/v1/core/pricing-option.json @@ -5,7 +5,10 @@ "description": "A pricing model option offered by a publisher for a product. Each pricing model has its own schema with model-specific requirements.", "oneOf": [ { - "$ref": "/schemas/v1/pricing-options/cpm-option.json" + "$ref": "/schemas/v1/pricing-options/cpm-fixed-option.json" + }, + { + "$ref": "/schemas/v1/pricing-options/cpm-auction-option.json" }, { "$ref": "/schemas/v1/pricing-options/cpc-option.json" diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index ad23660688..8622c8ae43 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -159,31 +159,35 @@ } }, "pricing-options": { - "description": "Individual pricing model schemas with model-specific validation", + "description": "Individual pricing model schemas with model-specific validation. CPM supports both fixed and auction pricing; all other models are fixed-rate only.", "schemas": { - "cpm-option": { - "$ref": "/schemas/v1/pricing-options/cpm-option.json", - "description": "Cost Per Mille (CPM) pricing option for display advertising" + "cpm-fixed-option": { + "$ref": "/schemas/v1/pricing-options/cpm-fixed-option.json", + "description": "Cost Per Mille (CPM) fixed-rate pricing for direct/guaranteed deals" + }, + "cpm-auction-option": { + "$ref": "/schemas/v1/pricing-options/cpm-auction-option.json", + "description": "Cost Per Mille (CPM) auction-based pricing for programmatic/non-guaranteed inventory" }, "cpc-option": { "$ref": "/schemas/v1/pricing-options/cpc-option.json", - "description": "Cost Per Click (CPC) pricing option for performance campaigns" + "description": "Cost Per Click (CPC) fixed-rate pricing for performance campaigns" }, "cpcv-option": { "$ref": "/schemas/v1/pricing-options/cpcv-option.json", - "description": "Cost Per Completed View (CPCV) pricing option for video/audio" + "description": "Cost Per Completed View (CPCV) fixed-rate pricing for video/audio" }, "cpv-option": { "$ref": "/schemas/v1/pricing-options/cpv-option.json", - "description": "Cost Per View (CPV) pricing option with threshold" + "description": "Cost Per View (CPV) fixed-rate pricing with threshold" }, "cpp-option": { "$ref": "/schemas/v1/pricing-options/cpp-option.json", - "description": "Cost Per Point (CPP) pricing option for TV/audio with demographic measurement" + "description": "Cost Per Point (CPP) fixed-rate pricing for TV/audio with demographic measurement" }, "flat-rate-option": { "$ref": "/schemas/v1/pricing-options/flat-rate-option.json", - "description": "Flat rate pricing option for DOOH and sponsorships" + "description": "Flat rate pricing for DOOH and sponsorships" } } }, diff --git a/static/schemas/v1/pricing-options/cpc-option.json b/static/schemas/v1/pricing-options/cpc-option.json index 97ffa8f87a..0114da7541 100644 --- a/static/schemas/v1/pricing-options/cpc-option.json +++ b/static/schemas/v1/pricing-options/cpc-option.json @@ -2,12 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/pricing-options/cpc-option.json", "title": "CPC Pricing Option", - "description": "Cost Per Click pricing for performance-driven advertising campaigns", + "description": "Cost Per Click fixed-rate pricing for performance-driven advertising campaigns", "type": "object", "properties": { "pricing_option_id": { "type": "string", - "description": "Unique identifier for this pricing option within the product (e.g., 'cpc_usd_auction')" + "description": "Unique identifier for this pricing option within the product (e.g., 'cpc_usd_fixed')" }, "pricing_model": { "type": "string", @@ -16,7 +16,7 @@ }, "rate": { "type": "number", - "description": "CPC rate (cost per click)", + "description": "Fixed CPC rate (cost per click)", "minimum": 0 }, "currency": { @@ -25,66 +25,12 @@ "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP", "JPY"] }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" - }, - "price_guidance": { - "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", - "properties": { - "floor": { - "type": "number", - "description": "Minimum bid price - publisher will reject bids under this value", - "minimum": 0 - }, - "p25": { - "type": "number", - "description": "25th percentile winning price", - "minimum": 0 - }, - "p50": { - "type": "number", - "description": "Median winning price", - "minimum": 0 - }, - "p75": { - "type": "number", - "description": "75th percentile winning price", - "minimum": 0 - }, - "p90": { - "type": "number", - "description": "90th percentile winning price", - "minimum": 0 - } - }, - "required": ["floor"] - }, "min_spend_per_package": { "type": "number", "description": "Minimum spend requirement per package using this pricing option, in the specified currency", "minimum": 0 } }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], - "allOf": [ - { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } - }, - { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } - } - ], + "required": ["pricing_option_id", "pricing_model", "rate", "currency"], "additionalProperties": false } diff --git a/static/schemas/v1/pricing-options/cpcv-option.json b/static/schemas/v1/pricing-options/cpcv-option.json index 2527ebb4a5..f6d0e5e4f7 100644 --- a/static/schemas/v1/pricing-options/cpcv-option.json +++ b/static/schemas/v1/pricing-options/cpcv-option.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/pricing-options/cpcv-option.json", "title": "CPCV Pricing Option", - "description": "Cost Per Completed View (100% video/audio completion) pricing", + "description": "Cost Per Completed View (100% video/audio completion) fixed-rate pricing", "type": "object", "properties": { "pricing_option_id": { @@ -16,7 +16,7 @@ }, "rate": { "type": "number", - "description": "CPCV rate (cost per 100% completion)", + "description": "Fixed CPCV rate (cost per 100% completion)", "minimum": 0 }, "currency": { @@ -25,66 +25,12 @@ "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP", "JPY"] }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" - }, - "price_guidance": { - "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", - "properties": { - "floor": { - "type": "number", - "description": "Minimum bid price - publisher will reject bids under this value", - "minimum": 0 - }, - "p25": { - "type": "number", - "description": "25th percentile winning price", - "minimum": 0 - }, - "p50": { - "type": "number", - "description": "Median winning price", - "minimum": 0 - }, - "p75": { - "type": "number", - "description": "75th percentile winning price", - "minimum": 0 - }, - "p90": { - "type": "number", - "description": "90th percentile winning price", - "minimum": 0 - } - }, - "required": ["floor"] - }, "min_spend_per_package": { "type": "number", "description": "Minimum spend requirement per package using this pricing option, in the specified currency", "minimum": 0 } }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], - "allOf": [ - { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } - }, - { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } - } - ], + "required": ["pricing_option_id", "pricing_model", "rate", "currency"], "additionalProperties": false } diff --git a/static/schemas/v1/pricing-options/cpm-option.json b/static/schemas/v1/pricing-options/cpm-auction-option.json similarity index 61% rename from static/schemas/v1/pricing-options/cpm-option.json rename to static/schemas/v1/pricing-options/cpm-auction-option.json index c04122cf5a..4003e8f8a1 100644 --- a/static/schemas/v1/pricing-options/cpm-option.json +++ b/static/schemas/v1/pricing-options/cpm-auction-option.json @@ -1,37 +1,28 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/pricing-options/cpm-option.json", - "title": "CPM Pricing Option", - "description": "Cost Per Mille (cost per 1,000 impressions) pricing for display and banner advertising", + "$id": "/schemas/v1/pricing-options/cpm-auction-option.json", + "title": "CPM Auction Pricing Option", + "description": "Cost Per Mille (cost per 1,000 impressions) with auction-based pricing - common for programmatic/non-guaranteed inventory", "type": "object", "properties": { "pricing_option_id": { "type": "string", - "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_guaranteed')" + "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_auction')" }, "pricing_model": { "type": "string", "const": "cpm", "description": "Cost per 1,000 impressions" }, - "rate": { - "type": "number", - "description": "CPM rate (cost per 1,000 impressions)", - "minimum": 0 - }, "currency": { "type": "string", "description": "ISO 4217 currency code", "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP", "JPY"] }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" - }, "price_guidance": { "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", + "description": "Pricing guidance for auction-based CPM bidding", "properties": { "floor": { "type": "number", @@ -67,24 +58,6 @@ "minimum": 0 } }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed"], - "allOf": [ - { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } - }, - { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } - } - ], + "required": ["pricing_option_id", "pricing_model", "price_guidance", "currency"], "additionalProperties": false } diff --git a/static/schemas/v1/pricing-options/cpm-fixed-option.json b/static/schemas/v1/pricing-options/cpm-fixed-option.json new file mode 100644 index 0000000000..87c33c2ba6 --- /dev/null +++ b/static/schemas/v1/pricing-options/cpm-fixed-option.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/pricing-options/cpm-fixed-option.json", + "title": "CPM Fixed Rate Pricing Option", + "description": "Cost Per Mille (cost per 1,000 impressions) with guaranteed fixed rate - common for direct/guaranteed deals", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product (e.g., 'cpm_usd_guaranteed')" + }, + "pricing_model": { + "type": "string", + "const": "cpm", + "description": "Cost per 1,000 impressions" + }, + "rate": { + "type": "number", + "description": "Fixed CPM rate (cost per 1,000 impressions)", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP", "JPY"] + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + } + }, + "required": ["pricing_option_id", "pricing_model", "rate", "currency"], + "additionalProperties": false +} diff --git a/static/schemas/v1/pricing-options/cpp-option.json b/static/schemas/v1/pricing-options/cpp-option.json index 9cfc6c113e..5ee9ed0d22 100644 --- a/static/schemas/v1/pricing-options/cpp-option.json +++ b/static/schemas/v1/pricing-options/cpp-option.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/pricing-options/cpp-option.json", "title": "CPP Pricing Option", - "description": "Cost Per Point (Gross Rating Point) pricing for TV and audio campaigns requiring demographic measurement", + "description": "Cost Per Point (Gross Rating Point) fixed-rate pricing for TV and audio campaigns requiring demographic measurement", "type": "object", "properties": { "pricing_option_id": { @@ -16,7 +16,7 @@ }, "rate": { "type": "number", - "description": "CPP rate (cost per rating point)", + "description": "Fixed CPP rate (cost per rating point)", "minimum": 0 }, "currency": { @@ -25,42 +25,6 @@ "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP", "JPY"] }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" - }, - "price_guidance": { - "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", - "properties": { - "floor": { - "type": "number", - "description": "Minimum bid price - publisher will reject bids under this value", - "minimum": 0 - }, - "p25": { - "type": "number", - "description": "25th percentile winning price", - "minimum": 0 - }, - "p50": { - "type": "number", - "description": "Median winning price", - "minimum": 0 - }, - "p75": { - "type": "number", - "description": "75th percentile winning price", - "minimum": 0 - }, - "p90": { - "type": "number", - "description": "90th percentile winning price", - "minimum": 0 - } - }, - "required": ["floor"] - }, "parameters": { "type": "object", "description": "CPP-specific parameters for demographic targeting and GRP requirements", @@ -85,24 +49,6 @@ "minimum": 0 } }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed", "parameters"], - "allOf": [ - { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } - }, - { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } - } - ], + "required": ["pricing_option_id", "pricing_model", "rate", "currency", "parameters"], "additionalProperties": false } diff --git a/static/schemas/v1/pricing-options/cpv-option.json b/static/schemas/v1/pricing-options/cpv-option.json index dd9a91ef76..827678b6a3 100644 --- a/static/schemas/v1/pricing-options/cpv-option.json +++ b/static/schemas/v1/pricing-options/cpv-option.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/pricing-options/cpv-option.json", "title": "CPV Pricing Option", - "description": "Cost Per View (at publisher-defined threshold) pricing for video/audio", + "description": "Cost Per View (at publisher-defined threshold) fixed-rate pricing for video/audio", "type": "object", "properties": { "pricing_option_id": { @@ -16,7 +16,7 @@ }, "rate": { "type": "number", - "description": "CPV rate (cost per view)", + "description": "Fixed CPV rate (cost per view)", "minimum": 0 }, "currency": { @@ -25,42 +25,6 @@ "pattern": "^[A-Z]{3}$", "examples": ["USD", "EUR", "GBP", "JPY"] }, - "is_fixed": { - "type": "boolean", - "description": "Whether this is a fixed rate (true) or auction-based (false)" - }, - "price_guidance": { - "type": "object", - "description": "Pricing guidance for auction-based pricing (required if is_fixed is false)", - "properties": { - "floor": { - "type": "number", - "description": "Minimum bid price - publisher will reject bids under this value", - "minimum": 0 - }, - "p25": { - "type": "number", - "description": "25th percentile winning price", - "minimum": 0 - }, - "p50": { - "type": "number", - "description": "Median winning price", - "minimum": 0 - }, - "p75": { - "type": "number", - "description": "75th percentile winning price", - "minimum": 0 - }, - "p90": { - "type": "number", - "description": "90th percentile winning price", - "minimum": 0 - } - }, - "required": ["floor"] - }, "parameters": { "type": "object", "description": "CPV-specific parameters defining the view threshold", @@ -98,24 +62,6 @@ "minimum": 0 } }, - "required": ["pricing_option_id", "pricing_model", "currency", "is_fixed", "parameters"], - "allOf": [ - { - "if": { - "properties": { "is_fixed": { "const": true } } - }, - "then": { - "required": ["rate"] - } - }, - { - "if": { - "properties": { "is_fixed": { "const": false } } - }, - "then": { - "required": ["price_guidance"] - } - } - ], + "required": ["pricing_option_id", "pricing_model", "rate", "currency", "parameters"], "additionalProperties": false } From 0ab5654dc3155f69b4b9297c7483beed1e53be47 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 15:58:02 -0400 Subject: [PATCH 14/16] fix: address schema review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes based on user feedback: 1. **format_ids now optional with smart default** - If omitted, defaults to all formats supported by the product - Useful when buyer doesn't know exact format details yet (HTML vs JS, etc.) - Still validates minItems: 1 when provided 2. **package budget changed from number to budget object** - Restored budget.$ref to /schemas/v1/core/budget.json - Allows structured budget allocation per package 3. **bid_price description updated** - Changed from "is_fixed: false" to "cpm-auction-option" - More accurate now that only CPM supports auction 4. **adcp_version reverted to 1.6.1** - Version bumps will be managed separately from feature work - Keeps this PR focused on pricing models 5. **Removed all deprecated pricing fields from product.json** - Deleted: is_fixed_price, cpm, currency, min_spend, floor_cpm, recommended_cpm - pricing_options is now the only way to declare pricing - Simplified validation: removed allOf with pricing oneOf - Made pricing_options required This completes the migration from legacy CPM-only pricing to the new multi-model pricing system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/core/product.json | 53 ++----------------- .../media-buy/create-media-buy-request.json | 2 +- .../schemas/v1/media-buy/package-request.json | 12 ++--- 3 files changed, 11 insertions(+), 56 deletions(-) diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json index dea1d73ff2..f355deca84 100644 --- a/static/schemas/v1/core/product.json +++ b/static/schemas/v1/core/product.json @@ -54,41 +54,11 @@ }, "minItems": 1 }, - "is_fixed_price": { - "type": "boolean", - "description": "DEPRECATED: Use pricing_options instead. Whether this product has fixed pricing (true) or uses auction (false)" - }, - "cpm": { - "type": "number", - "description": "DEPRECATED: Use pricing_options instead. Cost per thousand impressions", - "minimum": 0 - }, - "currency": { - "type": "string", - "description": "ISO 4217 currency code (for products using legacy cpm field)", - "pattern": "^[A-Z]{3}$", - "examples": ["USD", "EUR", "GBP"] - }, - "min_spend": { - "type": "number", - "description": "DEPRECATED: Use pricing_options[].min_spend_per_package instead. Minimum budget requirement", - "minimum": 0 - }, "estimated_exposures": { "type": "integer", "description": "Estimated exposures/impressions for guaranteed products", "minimum": 0 }, - "floor_cpm": { - "type": "number", - "description": "DEPRECATED: Use pricing_options with price_guidance instead. Minimum CPM for non-guaranteed products", - "minimum": 0 - }, - "recommended_cpm": { - "type": "number", - "description": "DEPRECATED: Use pricing_options with price_guidance instead. Recommended CPM to achieve exposures target", - "minimum": 0 - }, "measurement": { "$ref": "/schemas/v1/core/measurement.json" }, @@ -127,26 +97,12 @@ "description": "Expiration timestamp for custom products" } }, - "allOf": [ + "oneOf": [ { - "oneOf": [ - { - "required": ["properties"] - }, - { - "required": ["property_tags"] - } - ] + "required": ["properties"] }, { - "oneOf": [ - { - "required": ["pricing_options"] - }, - { - "required": ["is_fixed_price"] - } - ] + "required": ["property_tags"] } ], "required": [ @@ -155,7 +111,8 @@ "description", "format_ids", "delivery_type", - "delivery_measurement" + "delivery_measurement", + "pricing_options" ], "additionalProperties": false } \ No newline at end of file diff --git a/static/schemas/v1/media-buy/create-media-buy-request.json b/static/schemas/v1/media-buy/create-media-buy-request.json index aa57c3bdf7..e280869f28 100644 --- a/static/schemas/v1/media-buy/create-media-buy-request.json +++ b/static/schemas/v1/media-buy/create-media-buy-request.json @@ -9,7 +9,7 @@ "type": "string", "description": "AdCP schema version for this request", "pattern": "^\\d+\\.\\d+\\.\\d+$", - "default": "1.8.0" + "default": "1.6.1" }, "buyer_ref": { "type": "string", diff --git a/static/schemas/v1/media-buy/package-request.json b/static/schemas/v1/media-buy/package-request.json index 27260592c3..79f64ffbd1 100644 --- a/static/schemas/v1/media-buy/package-request.json +++ b/static/schemas/v1/media-buy/package-request.json @@ -23,7 +23,7 @@ }, "format_ids": { "type": "array", - "description": "Array of format IDs that will be used for this package - must be supported by the product", + "description": "Array of format IDs that will be used for this package - must be supported by the product. If omitted, defaults to all formats supported by the product.", "items": { "type": "string", "description": "Format ID referencing a format from list_creative_formats" @@ -31,9 +31,7 @@ "minItems": 1 }, "budget": { - "type": "number", - "description": "Budget allocation for this package in the media buy's currency", - "minimum": 0 + "$ref": "/schemas/v1/core/budget.json" }, "pacing": { "$ref": "/schemas/v1/enums/pacing.json" @@ -44,7 +42,7 @@ }, "bid_price": { "type": "number", - "description": "Bid price for auction-based pricing (required if the selected pricing_option has is_fixed: false)", + "description": "Bid price for auction-based CPM pricing (required if using cpm-auction-option)", "minimum": 0 }, "targeting_overlay": { @@ -59,8 +57,8 @@ } }, "anyOf": [ - {"required": ["buyer_ref", "product_id", "format_ids", "budget", "pricing_option_id"]}, - {"required": ["buyer_ref", "products", "format_ids", "budget", "pricing_option_id"]} + {"required": ["buyer_ref", "product_id", "budget", "pricing_option_id"]}, + {"required": ["buyer_ref", "products", "budget", "pricing_option_id"]} ], "additionalProperties": false } From 1abbbad91d5201073a52ffa1b65b057318868fd0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 16:03:40 -0400 Subject: [PATCH 15/16] fix: change package budget from object to number to avoid pacing redundancy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The budget.json schema includes an optional pacing field, but package-request also had a separate top-level pacing field, creating redundancy and confusion. Changed package budget from budget object to simple number: - Media buy level: budget object with total + currency - Package level: budget number (allocation within media buy's currency) + separate pacing field This makes semantic sense: - Media buy declares the overall budget and currency - Each package gets a numeric allocation from that budget - Pacing is a package-level delivery concern, not a budget concern Avoids the "why both?" question about budget.pacing vs top-level pacing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/v1/media-buy/package-request.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/schemas/v1/media-buy/package-request.json b/static/schemas/v1/media-buy/package-request.json index 79f64ffbd1..2f22e97332 100644 --- a/static/schemas/v1/media-buy/package-request.json +++ b/static/schemas/v1/media-buy/package-request.json @@ -31,7 +31,9 @@ "minItems": 1 }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Budget allocation for this package in the media buy's currency", + "minimum": 0 }, "pacing": { "$ref": "/schemas/v1/enums/pacing.json" From 6708130a5842b1def3c0a659399c0798ed7c7063 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 12 Oct 2025 19:38:40 -0400 Subject: [PATCH 16/16] refactor: simplify budget to plain number, remove currency redundancy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates budget.json schema in favor of plain number type throughout. Currency is now only declared in pricing options (single source of truth). Changes: - Remove static/schemas/v1/core/budget.json (no longer needed) - Change budget from object to number in: - create-media-buy-request.json - update-media-buy-request.json (both media buy and package level) - package.json - Remove budget from schema registry (index.json) - Update test files to remove budget schema references - Update README to remove budget.json from directory listing All tests pass. Budget is now a simple number with currency determined by the selected pricing_option_id. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/schemas/README.md | 1 - static/schemas/v1/core/budget.json | 25 ------------------- static/schemas/v1/core/package.json | 4 ++- static/schemas/v1/index.json | 4 --- .../media-buy/create-media-buy-request.json | 4 ++- .../media-buy/update-media-buy-request.json | 8 ++++-- tests/example-validation.test.js | 8 ------ tests/schema-validation.test.js | 3 +-- 8 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 static/schemas/v1/core/budget.json diff --git a/static/schemas/README.md b/static/schemas/README.md index a64d16f685..5cb594646c 100644 --- a/static/schemas/README.md +++ b/static/schemas/README.md @@ -21,7 +21,6 @@ schemas/ │ │ ├── package.json │ │ ├── creative-asset.json │ │ ├── targeting.json -│ │ ├── budget.json │ │ ├── frequency-cap.json │ │ ├── format.json │ │ ├── measurement.json diff --git a/static/schemas/v1/core/budget.json b/static/schemas/v1/core/budget.json deleted file mode 100644 index b8170215e7..0000000000 --- a/static/schemas/v1/core/budget.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/core/budget.json", - "title": "Budget", - "description": "Budget configuration for a media buy or package. Budget is always expressed as a monetary amount - the pricing model determines what is being purchased.", - "type": "object", - "properties": { - "total": { - "type": "number", - "description": "Total budget amount in the specified currency", - "minimum": 0 - }, - "currency": { - "type": "string", - "description": "ISO 4217 currency code", - "pattern": "^[A-Z]{3}$", - "examples": ["USD", "EUR", "GBP"] - }, - "pacing": { - "$ref": "/schemas/v1/enums/pacing.json" - } - }, - "required": ["total", "currency"], - "additionalProperties": false -} \ No newline at end of file diff --git a/static/schemas/v1/core/package.json b/static/schemas/v1/core/package.json index 8d24b94194..602e83e55f 100644 --- a/static/schemas/v1/core/package.json +++ b/static/schemas/v1/core/package.json @@ -18,7 +18,9 @@ "description": "ID of the product this package is based on" }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Budget allocation for this package in the currency specified by the pricing option", + "minimum": 0 }, "impressions": { "type": "number", diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json index 8622c8ae43..e1ec7961e6 100644 --- a/static/schemas/v1/index.json +++ b/static/schemas/v1/index.json @@ -35,10 +35,6 @@ "$ref": "/schemas/v1/core/targeting.json", "description": "Audience targeting criteria" }, - "budget": { - "$ref": "/schemas/v1/core/budget.json", - "description": "Budget configuration for a media buy or package" - }, "frequency-cap": { "$ref": "/schemas/v1/core/frequency-cap.json", "description": "Frequency capping settings" diff --git a/static/schemas/v1/media-buy/create-media-buy-request.json b/static/schemas/v1/media-buy/create-media-buy-request.json index e280869f28..04f711362e 100644 --- a/static/schemas/v1/media-buy/create-media-buy-request.json +++ b/static/schemas/v1/media-buy/create-media-buy-request.json @@ -47,7 +47,9 @@ "description": "Campaign end date/time in ISO 8601 format" }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Total budget for this media buy. Currency is determined by the pricing_option_id selected in each package.", + "minimum": 0 }, "reporting_webhook": { "allOf": [ diff --git a/static/schemas/v1/media-buy/update-media-buy-request.json b/static/schemas/v1/media-buy/update-media-buy-request.json index 20631ef624..cce04138c1 100644 --- a/static/schemas/v1/media-buy/update-media-buy-request.json +++ b/static/schemas/v1/media-buy/update-media-buy-request.json @@ -32,7 +32,9 @@ "description": "New end date/time in ISO 8601 format" }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Updated total budget for this media buy. Currency is determined by the pricing_option_id selected in each package.", + "minimum": 0 }, "packages": { "type": "array", @@ -49,7 +51,9 @@ "description": "Buyer's reference for the package to update" }, "budget": { - "$ref": "/schemas/v1/core/budget.json" + "type": "number", + "description": "Updated budget allocation for this package in the currency specified by the pricing option", + "minimum": 0 }, "active": { "type": "boolean", diff --git a/tests/example-validation.test.js b/tests/example-validation.test.js index 92497bdcff..ef3cf4130d 100644 --- a/tests/example-validation.test.js +++ b/tests/example-validation.test.js @@ -354,14 +354,6 @@ test('Targeting example validates against schema', () => { ); }); -test('Budget example validates against schema', () => { - return validateAgainstSchema( - exampleData.budget, - '/schemas/v1/core/budget.json', - 'Budget example' - ); -}); - test('Frequency Cap example validates against schema', () => { return validateAgainstSchema( exampleData.frequencyCap, diff --git a/tests/schema-validation.test.js b/tests/schema-validation.test.js index 634394c999..7b89e95e34 100644 --- a/tests/schema-validation.test.js +++ b/tests/schema-validation.test.js @@ -276,8 +276,7 @@ async function runTests() { 'media-buy.json': ['media_buy_id', 'status', 'promoted_offering', 'total_budget', 'packages'], 'package.json': ['package_id', 'status'], 'creative-asset.json': ['creative_id', 'name', 'format'], - 'error.json': ['code', 'message'], - 'budget.json': ['total', 'currency'] // Budget is always a monetary amount + 'error.json': ['code', 'message'] }; for (const [schemaPath, schema] of coreSchemas) {