Repository navigation
Description
What did you build?
A mapping to an existing OMS / ad server / DSP data model
What you tried to express, and what happened
Filed during the extended public comment period, to November 10, 2026.
About our build.
We tested an unreleased seller-agent branch that maps Google Ad Manager (GAM) objects and seller-declared inventory to OpenProposal line items: IABTechLab/seller-agent, branch feat/openproposal-mapping at e77022e, with its line-item model in src/ad_seller/models/openproposal.py (Pydantic v2, default settings). We used synthetic data only, in IAB Australia's AAMP lab, on October 27, 2026. The archived request and response files for all six checks (D1 to D6), and the results summary, are byte-identical across three runs in two environments: two in a container and one under WSL2 on a separate machine, all with Python 3.12 and Pydantic 2.12.5.
Separately, we checked the D6 input against an independent, unofficial JSON Schema generated from the draft (details below). None of these results is an official conformance finding.
Section references are to spec/openproposal-3.0.md at 44dfaaf.
We tried to express a DOOH line item for one synthetic Melbourne screen: a 10-second spot in a 60-second loop, one-sixth share of voice, 14.2 audience impressions per play, a committed audience figure and an AUD 30 CPM.
Through this branch's mapping (D1 to D4).
- D1: a synthetic GAM line item for a portrait DOOH screen network, with
environmentType: BROWSER, came out aschannel: display. - D2: the same line item as a 10-second video, with
environmentType: VIDEO_PLAYER, came out aschannel: video, with the branch's note "channel: VIDEO_PLAYER cannot be told apart from CTV; mapped to video". This tested a 10-second duration, not loop scheduling. - D3: a GAM ad unit for one screen came out as
channel: display, withenvironmentsset toweb_desktopandweb_mobile. - D4: the branch's many-to-one builder combined two seller-declared screen objects marked
doohinto onechannel: doohline item. Each screen became aproperties.availableentry withid,nameandsupported_formats. The line item had noenvironments, because the branch's channel-to-environment table has no entry fordooh.
These are observations of this branch's mapper output, not limits of the draft's channel or environments vocabulary.
Line-item test (D6). We validated one DOOH line item against the branch's LineItem model. The values exercise field handling; they aren't a validated commercial offer. The venue value and format identifier are synthetic test strings, not registered taxonomy or format values. MOVE appears as an illustrative measurement label; the fixture doesn't claim the figures were measured or endorsed by MOVE.
D6 input, exactly as archived
{
"line_item_id": "wattlegum-cbd-portrait-network",
"status": "published",
"channel": "dooh",
"properties": {
"available": [
{
"id": "wg-mel-001",
"name": "Collins St Tram Shelter 1",
"domains": [],
"supported_formats": [
"dooh/portrait-1080x1920-10s"
]
}
],
"included": [
"wg-mel-001"
]
},
"environments": {
"available": [
"dooh"
],
"included": [
"dooh"
]
},
"formats": {
"available": [
"dooh/portrait-1080x1920-10s"
],
"included": [
"dooh/portrait-1080x1920-10s"
]
},
"geo": {
"allowed": [
"AU-VIC"
]
},
"daypart": {
"allowed": [
"06:00-10:00",
"15:00-19:00"
]
},
"frequency_cap": {
"max": {
"min": 1,
"max": 1
},
"period": "day",
"basis": "screen"
},
"pricing": [
{
"cost_method": "CPM",
"gross_rate": 30.0,
"currency": "AUD"
}
],
"committed_metrics": [
{
"metric": "audience_impacts",
"value": 1200000,
"basis": "MOVE"
}
],
"venue_type": "transit.tram_shelter",
"loop_length_s": 60,
"spot_length_s": 10,
"share_of_voice": 0.1667,
"impression_multiplier": {
"multiplier": 14.2,
"sourcetype": 1,
"vendor": "MOVE"
},
"screen_location": {
"lat": -37.8136,
"lon": 144.9631
}
}The test ran:
li = LineItem.model_validate(DESK_LINE) # DESK_LINE: the input above
dumped = li.model_dump(exclude_none=True)
dropped = [k for k in NO_HOME if k not in dumped] # NO_HOME: the six DOOH fields| Check | Observed in all three runs |
|---|---|
| Validation | Accepted, without a validation error |
venue_type, loop_length_s, spot_length_s, share_of_voice, impression_multiplier, screen_location |
All six missing from the output |
Any of the six kept under ext |
No; the output had no ext |
frequency_cap.basis: "screen" |
Kept as "screen" |
Independent schema check (separate from the October 27 runs). We also checked the D6 input on October 28, with Python jsonschema 4.26.0, against an independent, unofficial JSON Schema generated from the draft: KonstantinMirin/openproposal-schema at 159a254, as-written build, published at openproposal.noetis.dev. Its author reports on generating it in #121. It isn't an IAB Tech Lab artifact, and where the draft leaves a shape or constraint unspecified, its rules are its author's choices. The schema rejected the input. Because it closes every object the draft defines, it treats each of the six DOOH fields as a property the line item doesn't define. It also rejects basis: "screen" and the committed metric (below). Its remaining failures come from its own format for daypart windows. So the same line item is trimmed without an error by one tool and rejected under the other's rules.
Update — October 29, 2026. The independent schema maintainer corrected its treatment of committed metric values. Our recheck at e3870c4 confirms that 1,200,000 no longer fails the quantity constraint; using example.com:audience_impacts also removes the metric-name failure. Other D6 validation failures remain. The October 28 result above records the earlier schema version. See [the follow-up verification](#134 (comment)).
The alternative DOOH design below also needs a qualification: derive share of voice from slot and loop length only for sov_basis: loop_time. For sov_basis: impressions, carry the agreed share explicitly across the eligible inventory pool, without requiring a fixed slot or loop.
None of the fields in § 5.2 to § 5.7 gives the terms that define a DOOH buy a standardized home:
| Term | What it tells a buyer | Nearest field in the draft | Gap |
|---|---|---|---|
| Venue type | What kind of place the screen is in (rail platform, airport, shopping center) | environments (dooh only); properties (id, name, domains, supported_formats) |
No standardized venue field or taxonomy |
| Screen location | Where each screen is | geo (§ 5.5), a targeting envelope of geo codes |
Says what a buyer may select, not where each screen is |
| Spot length | Length of one play | formats and specs (§ 5.4), through the format registry |
The registry has no dooh/* entries (open question 4) |
| Loop length | How often the spot comes around | None | Not a creative spec, so the registry can't hold it |
| Share of voice | Share of the loop the buyer gets | None | It's what a guaranteed DOOH package sells |
| Impressions per play | Audience impressions one play represents | None | Without it, a DOOH CPM is ambiguous |
The draft's descriptive fields could hold these terms as free text or ad hoc structure: specifications on the proposal (§ 4.2, "structured detail at media-kit depth") or content_detail on the line item (§ 5.2). Neither gives them agreed names or meaning, so the draft provides no shared interpretation for receiving agents.
Open question 4 (#5) notes that the format registry has no dooh/* entries. A registry entry can hold creative specs such as spot length, but the other five terms are inventory, commercial and measurement terms that a registry entry can't hold. Open question 7 (#8) asks sellers to map real inventory onto environments and report what doesn't fit. This is that report for DOOH: dooh fits as a value, but venue type, which tells one DOOH environment from another, has no standardized field in § 5.2.
§ 5.7 says a DOOH guarantee is "an audience-modelled metric for DOOH", but doesn't name the metric or say whether its value is a volume or a rate. D6 committed 1,200,000 audience_impacts. The branch's model kept it as given. The independent schema rejected both the name, because the only bare metric names it accepts are viewable_rate and completion_rate, and the value, because it treats every committed metric value as a fraction from 0 to 1. Nor does the line item have a standardized field for the per-play audience figure the metric is modeled from.
The price basis is ambiguous. Taking D6's own figures as arithmetic, not as a test result: at AUD 30 CPM with 14.2 audience impressions per play, one play costs AUD 0.426 if the CPM is per audience impression, as OpenRTB 2.6 bills with the imp.qty multiplier, or AUD 0.03 if it's read as a CPM per play. That's a 14.2-fold difference a buying agent can't resolve from the draft.
For PMP and open-market buys, the line item lands in OpenRTB 2.6 (§ 1.1), which already carries DOOH: the dooh object (§ 3.2.32) with venuetype and venuetypetax, imp.qty (§ 3.2.31) with multiplier, sourcetype and vendor, and imp.dt for when the play is expected. The proposal stage is the gap in that chain.
This builds on #5 and #8. Open change requests already draft much of the fix: #96 (venue type, screens, operating hours and loop, on the property object), #93 (delivery_type, including share_of_voice), #92 (what a play is, and the multiplier that turns plays into impressions) and #79 (share of voice and loop constraints). The evidence here supports them. How a receiver should treat fields it doesn't recognize is #33; a companion comment there presents this evidence (comment).
Suggested change
Much of this is drafted in open change requests: #96 puts venue type, screens with their location, operating hours and loop on the property object; #93 adds delivery_type, including share_of_voice; and #92 defines a play, says whose multiplier turns plays into impressions, and names impressions as the DOOH committed metric. Whichever shape the working group adopts, our evidence says it needs to:
- give each of the six terms D6 lost a defined home: venue type with its taxonomy and version, screen location, loop length, spot length, share of voice, and impressions per play with its measurement source;
- derive share of voice from the loop and slot terms, rather than carry it as a separate value that can disagree with them;
- make the price basis explicit, since D6's two readings differ 14.2-fold.
#96, #93 and #92 cover most of this. Three points they leave open:
- Committed metric values. With
impressionsas the DOOH metric ([Change] § 5.6, § 5.7 — DOOH: define a play, and whose multiplier turns plays into impressions #92), § 5.7 should also say whether each metric'svalueis a rate or a count: the "metric and unit" the replies on OQ-3 — Cross-channel guarantee comparability is unsolved #4 ask for. The draft doesn't say, and the independent schema treats every committed value as a fraction from 0 to 1, so it rejected D6's 1,200,000. - Units. [Change] § 5.2, § 5.5 — DOOH properties: venue type, screens, operating hours, and loop #96 now says which quantity is committed for each kind of DOOH product, and premesh's list on OQ-7 —
environments[]needs validation against real inventory #8 names the units sellers use: impressions, plays, slots per loop and unit-periods. List them as core terms foravailability.unitandcommitment_bounds, so a line item that counts only plays, as [Change] § 5.6, § 5.7 — DOOH: define a play, and whose multiplier turns plays into impressions #92 allows, can state its quantity. - Taxonomy versions. A venue type's taxonomy identifier comes from AdCOM's DOOH Venue Taxonomies list, as in [Change] § 5.2, § 5.5 — DOOH properties: venue type, screens, operating hours, and loop #96's
{ taxonomy, id }. That list identifies OpenOOH 1.0 (1) and 1.1 (5) but has no entry for OpenOOH 1.2.1 (February 2026). Version 1.2.1 added tram categories, such as 10803 Tram Station, which fit the tram-shelter screen we tested. In 1.1 the nearest category is 106 Train Stations, which is what OpenOOH's own 1.2-to-1.1 mapping uses. Say how a line item identifies a taxonomy version the AdCOM list doesn't cover yet, and that one version's identifier doesn't cover later versions.
For comparison with #96, we also offer an alternative shape: a channel-keyed dooh object on the line item, keyed to property ids, which leaves the property option the same for every channel. #96 covers operating hours, time zones, seller-allocated screens and sync groups, which this shape doesn't, and #92's impression_multiplier covers more of the multiplier than this shape's multiplier_source.
Alternative shape: a line-item dooh object
A dooh object on the line item, REQUIRED when channel is dooh and absent otherwise. Buyers select screens through properties (§ 5.2), as for any other channel; the dooh object adds what a property option can't hold. Each field carries one mutability marker (§ 3). Where OpenRTB 2.6 or AdCOM already has the concept, the field aligns with it.
| Field | Type and constraints | Mutability | Meaning | Aligns with (OpenRTB 2.6 / AdCOM) |
|---|---|---|---|---|
dooh.venue_taxonomy |
Integer, REQUIRED | seller-set | An explicit value from AdCOM's DOOH Venue Taxonomies list, identifying the taxonomy and version used for every screen's venue_type (for example, 5 = OpenOOH 1.1). One version's identifier doesn't cover later versions. REQUIRED rather than defaulted, because OpenRTB 2.6's default of 1 means OpenOOH 1.0 |
dooh.venuetypetax |
dooh.screens |
Object, REQUIRED | seller-set | One entry per properties.available[] option, keyed by its id: { venue_type, geo: { lat, lon }, impressions_per_play }. venue_type (the venue's code in that taxonomy; for OpenOOH, its integer enumeration ID) and geo are REQUIRED. impressions_per_play is a number greater than 0, REQUIRED when any price or commitment is in audience impressions |
dooh.id, dooh.venuetype, device.geo, imp.qty.multiplier |
dooh.multiplier_source |
Object | seller-set | { sourcetype, vendor }: who measured impressions_per_play, for every screen in the line item. REQUIRED when any screen has impressions_per_play. sourcetype is a value from AdCOM's DOOH Multiplier Measurement Source Types list |
imp.qty.sourcetype, imp.qty.vendor |
dooh.loop_length_s |
Integer, greater than 0 | seller-set | Fixed-loop inventory only. Loop length in seconds, the same for every screen in the line item. Screens on different loops go in separate line items | None |
dooh.slot |
Object | selectable | Fixed-loop inventory only. available[] of { id, spot_length_s, spots_per_loop }, both integers greater than 0, with pricing per option and max_select: 1. Declaring pairs, not two separate lists, means a buyer can only pick combinations the seller sells |
None |
dooh.share_of_voice |
Number, greater than 0 and at most 1 | derived | Fixed-loop inventory only. spot_length_s × spots_per_loop ÷ loop_length_s for the selected slot |
None |
Rules for the object:
- Fixed-loop inventory (sold as a share of a screen's loop):
loop_length_sandslotare REQUIRED.dooh.slot.included[]holds exactly one slotid, and every slot option satisfiesspot_length_s × spots_per_loop ≤ loop_length_s. - Inventory sold by the play, without a fixed loop (for example, programmatic DOOH): omit
loop_length_s,slotandshare_of_voice. Spot length comes fromformatsandspecs(§ 5.4), and quantity fromavailabilityin plays (clarification 2). - Every
properties.available[].idhas an entry indooh.screens, anddooh.screenshas no other keys. - Receivers compute
share_of_voicefrom the selected slot. If a document states a value, it MUST equal the computed value rounded to four decimal places; otherwise the receiver rejects the line item.
Design example, not a tested payload: a separate synthetic rail-platform line item that shows the proposed DOOH fields, not a complete line item. It uses OpenOOH 1.1 (AdCOM taxonomy identifier 5) and venue enumeration 10602 (Train Stations: Platform). The screen, slot ID, location, figures and vendor are illustrative, and the format ID is synthetic, since the format registry has no dooh/* entries yet (open question 4).
{
"line_item_id": "wattlegum-rail-design-example",
"status": "published",
"channel": "dooh",
"properties": {
"available": [
{ "id": "wg-rail-0412", "name": "Synthetic rail platform screen", "domains": [],
"supported_formats": ["dooh/portrait-1080x1920-10s"] }
],
"included": ["wg-rail-0412"]
},
"dooh": {
"venue_taxonomy": 5,
"screens": {
"wg-rail-0412": { "venue_type": 10602, "geo": { "lat": -37.81, "lon": 144.96 }, "impressions_per_play": 14.2 }
},
"multiplier_source": { "sourcetype": 1, "vendor": "example-measurement.com" },
"loop_length_s": 60,
"slot": {
"available": [{ "id": "10s-x1", "spot_length_s": 10, "spots_per_loop": 1 }],
"included": ["10s-x1"],
"max_select": 1
},
"share_of_voice": 0.1667
},
"pricing": [{ "cost_method": "CPM", "gross_rate": 30.0, "currency": "AUD" }]
}Why this shape:
- One object keyed to the channel respects the no-inheritance rule (§ 2) and keeps DOOH fields off every other line item.
- Buyers select screens through the existing
propertiesfield, which is where this branch already maps them (D4). Thedoohobject only adds what a property option can't hold. - The loop fields apply only where a seller sells a share of a fixed loop, so the object doesn't make loop scheduling mandatory for every DOOH buy.
- Aligning the concepts with OpenRTB and AdCOM reduces ambiguity when mapping proposals to execution.
- The new fields add no
requestablefields, so the number that needs a round trip (Appendix B) stays at three.
What you did instead
Nothing interoperable. The implementation dropped the terms without an error. We could have written them as free text in a descriptive field, but nothing would tell another agent what they mean, and the draft defines no ext.
Fields involved
specifications (§ 4.2); channel (§ 5.1); properties, content_detail and environments (§ 5.2); formats and specs (§ 5.4); geo and frequency_cap (§ 5.5); pricing[].cost_method, availability.unit and commitment_bounds (§ 5.6); committed_metrics[] (§ 5.7).
Before you submit
- I have read CONTRIBUTING.md and agree my contribution may be published under this repository's licence.
- This contains no confidential or commercially sensitive information — no real rate cards, deal terms, client names or inventory data.
Activity
KonstantinMirin commentedon Oct 28, 2026
Thank you for testing our schema, @jaanijuk. Treating every committed value as a fraction was our mistake.
Postindustria (IAB Tech Lab member) maintains the schema and has fixed it: see the corrected decision. A rate, such as
viewable_rate, is a fraction from 0 to 1. A quantity, such as DOOH audience impacts, is a non-negative number. The metric name check stays: a name the draft doesn't list is a namespaced extension term, such asexample.com:audience_impacts.Our mistake supports your point on #4: the draft should say whether each metric's
valueis a rate or a count, as #92's revision proposes.Analyzed and generated with the help of Claude, fully reviewed by me.
premesh commentedon Oct 28, 2026
Thanks for testing this against a real mapping. Agree on giving each term a defined home and making the price basis explicit.
One caveat on deriving share of voice from loop and slot. That holds for a fixed loop spot (
sov_basis: loop_timein #93): a 10-second slot in a 60-second loop is one-sixth. It doesn't hold for rotary products, where share of voice is a share of impressions across a pool of screens (sov_basis: impressions) and the creative doesn't sit in a fixed slot. So I'd derive it whensov_basisisloop_time, and carry it as a stated value whensov_basisisimpressions.jaanijuk commentedon Oct 28, 2026
Thanks @KonstantinMirin and @premesh — both points help clarify the proposal.
@KonstantinMirin, we rechecked the archived D6 input against the corrected independent schema at
e3870c4, using Pythonjsonschema4.26.0. The 1,200,000 value no longer fails the quantity constraint. With the metric renamed toexample.com:audience_impacts, there are no committed-metric errors; bareaudience_impactsstill fails the name check. The tested zero, negative and rate-boundary cases also behave as expected. The full payload retains its other validation failures.We’ll retain the October 28 result as evidence of the earlier schema version and add this correction alongside it. The remaining specification question is how each metric declares its meaning and whether its value is a rate or quantity, as discussed on #4 and #92.
@premesh, agreed: our derivation was limited to fixed-loop inventory. For
sov_basis: loop_time, share can be derived from spot length, spots per loop and loop length. Forsov_basis: impressions, it should be an explicitly agreed share across the eligible inventory pool, without requiring a fixed slot or loop.For that pooled share, could the definition also make explicit whether the denominator counts audience impressions or plays, which screens are eligible, and the flight/daypart scope? That would let agents distinguish the promised share from the delivered share.
premesh commentedon Oct 30, 2026
@jaanijuk yes to all three, and most of it already has a home:
sov_basisisimpressions. A share counted in plays is a different product and should say so with its own basis, not reuse this one.allocation: seller_allocated).Promised share is the offered term. Delivered share belongs in the settlement record, measured against the same denominator and pool, so the two are comparable.
KonstantinMirin commentedon Nov 4, 2026
Thanks, @jaanijuk and @premesh. We revised three filings from this thread and #8:
sov_share, the promised share undersov_basis: impressions, with @premesh's denominator, pool, and scope.availability.unit, as @jaanijuk's point 2 asks. Bounds in units sit on eachpricing[]entry asunit_bounds, counted in that entry's unit, as our pricing PR for #94 proposes.Analyzed and generated with the help of Claude, fully reviewed by me.