diff --git a/examples/line-item-audio.yaml b/examples/line-item-audio.yaml index 4ca2f21..03f28ea 100644 --- a/examples/line-item-audio.yaml +++ b/examples/line-item-audio.yaml @@ -14,6 +14,7 @@ production: pricing: { seller_built: { delta_flat: 12000.00 } } frequency_cap: { max: { min: 1, max: 6 }, period: "day", basis: listener } +time_zone: viewer_local pricing: - { cost_method: CPM, gross_rate: 18.00, floor: 15.84, diff --git a/examples/line-item-ctv.yaml b/examples/line-item-ctv.yaml index fda3994..0046dbf 100644 --- a/examples/line-item-ctv.yaml +++ b/examples/line-item-ctv.yaml @@ -10,6 +10,7 @@ formats: { available: ["ctv/instream-15s", "ctv/instream-30s"], creative_delivery: { method: seller_hosted, protocol: vast_4_2, secure_required: true } frequency_cap: { max: { min: 1, max: 4 }, period: "day", basis: household } +time_zone: viewer_local pricing: - { cost_method: CPM, gross_rate: 45.00, floor: 39.60, diff --git a/examples/line-item-display.yaml b/examples/line-item-display.yaml index 808af24..30479ef 100644 --- a/examples/line-item-display.yaml +++ b/examples/line-item-display.yaml @@ -49,6 +49,7 @@ materials_due: "2026-09-24" geo: { allowed: ["US-NY", "US-CA", "US-IL", "US-TX"] } frequency_cap: { max: { min: 1, max: 5 }, period: "day", basis: device } +time_zone: "America/New_York" pricing: - cost_method: CPM diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index 1dfacef..55bc65b 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -5,6 +5,13 @@ issue that caused it, so the comment record and the spec history line up. ## [Unreleased] +- § 5.5 — `time_zone` on the line item is the clock for `daypart` and + `frequency_cap.period`: an IANA time zone name or `viewer_local`, REQUIRED + with either. A `daypart` window is `{ days[], start, end }` in civil time, + with `end` exclusive and later than `start`; a window that crosses midnight + is two windows. `frequency_cap.period` is a calendar period in `time_zone`. + Breaking ([#111](https://github.com/IABTechLab/OpenProposal/issues/111)). + ## [3.0-draft-1] — 2026-09-22 Initial draft published for public comment. Ported from the working-group diff --git a/spec/openproposal-3.0.md b/spec/openproposal-3.0.md index 6940958..588096a 100644 --- a/spec/openproposal-3.0.md +++ b/spec/openproposal-3.0.md @@ -250,16 +250,39 @@ channel. ### 5.5 Targeting envelope -Each field declares bounds, not a value. A bare value means the field is -`seller-set`. +Each targeting field except `time_zone` declares bounds, not a value. A bare +value means the field is `seller-set`. | Field | Type | Mutability | Description | | :--- | :--- | :--- | :--- | | `geo` | Object | settable | `allowed[]` of geo codes the buyer may select within | | `device` | Object | settable | `allowed[]` of device categories | -| `daypart` | Object | settable | `allowed[]` of time windows | +| `daypart` | Object | settable | `allowed[]` of `{ days[], start, end }`: `days[]` from `monday` \| `tuesday` \| `wednesday` \| `thursday` \| `friday` \| `saturday` \| `sunday`, HH:MM civil time, `end` exclusive, up to `24:00` | +| `time_zone` | String | seller-set | IANA time zone name, or `viewer_local` (where the ad renders). The clock for `daypart` and `frequency_cap.period`. REQUIRED with either | | `frequency_cap` | Object | settable | `{ max: { min, max }, period, basis: device \| household \| listener }`. Basis matters: CTV typically caps at household, audio at listener | +**Window shape.** A `daypart` window is `{ days[], start, end }`, with `days[]` +from Monday to Sunday, `start` and `end` as HH:MM, and `end` exclusive, +later than `start`, and allowed to be `24:00`. A window that crosses midnight +is two windows: + +```yaml +# Late fringe, 23:00 to 01:00, Monday night into Tuesday +daypart: + allowed: + - { days: [monday], start: "23:00", end: "24:00" } + - { days: [tuesday], start: "00:00", end: "01:00" } +time_zone: "America/New_York" +``` + +**Clock.** `time_zone` names a zone in the +[IANA time zone database](https://www.iana.org/time-zones), such as +America/New_York or UTC. Windows follow daylight-saving rules: a skipped +hour matches no instant; a repeated hour matches both. Like the calendar Day of +[OpenDirect 2.1 `Line.frequencyinterval`](https://github.com/InteractiveAdvertisingBureau/OpenDirect/blob/82bc294314aba017fc639ba634aecc53adfd675f/OpenDirect.v2.1.final.md#object_line), +`frequency_cap.period` is a calendar period in `time_zone`; weeks start on +Monday. + ### 5.6 Commercial terms | Field | Type | Mutability | Description | @@ -469,6 +492,7 @@ body is normative.** | `geo` | 5.5 Targeting envelope | Object | settable | | `device` | 5.5 Targeting envelope | Object | settable | | `daypart` | 5.5 Targeting envelope | Object | settable | +| `time_zone` | 5.5 Targeting envelope | String | seller-set | | `frequency_cap` | 5.5 Targeting envelope | Object | settable | | `pricing[]` | 5.6 Commercial terms | Array[Object] | mixed | | `pricing[].cost_method` | 5.6 Commercial terms | Enum | seller-set | @@ -512,7 +536,7 @@ body is normative.** | Marker | Count | Meaning | Fields | | :--- | :--- | :--- | :--- | | immutable | 6 | Set once at creation. Neither party may change it. | `proposal_id`, `seller_id`, `seller_name`, `type`, `line_item_id`, `channel` | -| seller-set | 29 | Seller sets and may revise while `draft` or `published`. | `valid_from` · `valid_until`, `description`, `specifications`, `best_for[]`, `not_suitable_for[]`, `seasonality_notes`, `content_detail`, `specs`, `third_party_tags`, `creative_delivery`, `approval`, `pricing[].cost_method`, `pricing[].gross_rate`, `pricing[].seller_rate`, `pricing[].price_valid_until`, `pricing[].currency`, `pricing[].floor`, `commitment_bounds`, `min_spend`, `creative_policy`, `visibility`, `required_credentials[]`, `eligible_bidders[]`, `terms_acceptance_required`, `buying_route`, `serving_mode`, `serving_platform`, `billing`, `supported_order_systems[]` | +| seller-set | 30 | Seller sets and may revise while `draft` or `published`. | `valid_from` · `valid_until`, `description`, `specifications`, `best_for[]`, `not_suitable_for[]`, `seasonality_notes`, `content_detail`, `specs`, `third_party_tags`, `creative_delivery`, `approval`, `time_zone`, `pricing[].cost_method`, `pricing[].gross_rate`, `pricing[].seller_rate`, `pricing[].price_valid_until`, `pricing[].currency`, `pricing[].floor`, `commitment_bounds`, `min_spend`, `creative_policy`, `visibility`, `required_credentials[]`, `eligible_bidders[]`, `terms_acceptance_required`, `buying_route`, `serving_mode`, `serving_platform`, `billing`, `supported_order_systems[]` | | selectable | 12 | Buyer chooses from a declared set. Resolves without assent. | `properties`, `environments`, `audiences`, `activation_route`, `formats`, `production`, `cancellation_policy`, `exclusivity`, `measurement_source`, `shortfall_remedy`, `reporting`, `transaction_mechanism` | | settable | 5 | Buyer sets a value within declared bounds. Resolves without assent. | `geo`, `device`, `daypart`, `frequency_cap`, `commitment` | | supplied | 3 | Buyer provides data or an artifact. | `brief_ref`, `buyer_supplied`, `assets[]` | @@ -522,8 +546,8 @@ body is normative.** ### What the distribution says -Of 71 fields, **23 are touched by the buyer** — 12 `selectable`, 5 `settable`, 3 -`supplied` and 3 `requestable`. The remaining 48 are `seller-set`, `immutable`, +Of 72 fields, **23 are touched by the buyer** — 12 `selectable`, 5 `settable`, 3 +`supplied` and 3 `requestable`. The remaining 49 are `seller-set`, `immutable`, `derived` or `mixed`. Only the 3 `requestable` fields require a round trip. Everything else the buyer