OpenRTB protocol
CTV in OpenRTB: video object, ad pods, plcmt
There is no single CTV flag in OpenRTB. A connected TV request is recognized by a combination of signals: device.devicetype set to 3 (Connected TV) or 7 (Set Top Box), an app object instead of site, and a content object describing the show. OpenRTB 2.6 then added the fields CTV actually needed: ad pods, exact durations, duration floors, and an SSAI signal.
What marks a request as CTV
device.devicetype: 3 = Connected TV, 7 = Set Top Box, per the AdCOM 1.0 Device Types list. Value 6 (Connected Device) also shows up for streaming sticks. See the enum reference.- An
appobject, since CTV inventory is an app, not a browser page. Theapp.bundlecarries the store-assigned app ID. - A
contentobject underapp, with metadata about what is playing. Since 2.6 it can carrycontent.networkandcontent.channelobjects (each withid,name,domain), so a buyer can tell WABC-TV on ABC apart from every other stream the same exchange sells. imp.videowithplcmt= 1 (Instream): sound on by default, the video content is what the user came for. See placement vs plcmt for why the olderplacementfield was deprecated in 2.6-202303.
Ad pods: the 2.6 pod bidding fields
A CTV ad break is a pod: several ads back to back, like a linear TV break. Before 2.6 there was no standard way to sell one, so exchanges improvised with extensions. OpenRTB 2.6 added pod fields to imp.video (and imp.audio):
| Field | Type | Meaning |
|---|---|---|
podid | string | Identifier for the pod. Impressions sharing the same podid in one request belong to the same ad pod. |
podseq | integer, default 0 | Position of the pod within the content stream. AdCOM Pod Sequence: -1 last pod, 0 any pod, 1 first pod. |
slotinpod | integer, default 0 | Slot position the seller can guarantee within the pod. AdCOM Slot Position in Pod: -1 last, 0 any, 1 first, 2 first or last. Replaces the deprecated video.sequence. |
rqddurs | integer array | Exact acceptable creative durations in seconds, for live TV where imprecise durations cause dead air. Mutually exclusive with minduration and maxduration. |
poddur | integer, recommended | Total seconds advertisers may fill in a dynamic pod (the whole break), as opposed to per-slot duration constraints. |
maxseq | integer, recommended | Maximum number of ads that may be served into a dynamic pod. |
mincpmpersec | float | Price floor per second of duration for the dynamic portion of a pod. |
A structured pod is one imp per slot, all sharing a podid. A dynamic pod is a single imp with poddur and maxseq, letting bidders fill the break with creatives of varying lengths. On the response side, bid.slotinpod lets a bidder say its bid is only valid for a specific position.
Duration floors, rewarded, and SSAI
Three more 2.6 additions matter for CTV. imp.video.durfloors is an array of DurFloors objects, each with mindur, maxdur, and a bidfloor, so a 15 second spot and a 60 second spot in the same slot can carry different prices. Details on the bid floors page.
imp.rwdd (integer, default 0) flags a rewarded placement: the user gets something for watching, typically after video completion. It sits on the Imp object, not on video.
imp.ssai (integer, default 0) signals server-side ad insertion, which is how most CTV ads are delivered: 0 = unknown, 1 = all client-side, 2 = assets stitched server-side but trackers fired client-side, 3 = all server-side. Note the path: it lives on imp, not inside the video object. SSAI also changes identity handling: the stitching server originates the requests, so device.ifa and the device IP must be passed through from the actual device rather than reflecting the server, or frequency capping and fraud detection break.
The full 2.5 to 2.6 delta, including everything above, is in OpenRTB 2.5 vs 2.6.
Validate it
CTV requests are where stale fields concentrate: video.sequence instead of slotinpod, placement instead of plcmt, pod fields typo'd into ext. Paste a request into the bid request tester and rtblint flags deprecated and moved fields with stable rule IDs such as openrtb.field.deprecated, plus unknown fields via openrtb.field.undefined. The same checks run in CI through the CLI.