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 app object, since CTV inventory is an app, not a browser page. The app.bundle carries the store-assigned app ID.
  • A content object under app, with metadata about what is playing. Since 2.6 it can carry content.network and content.channel objects (each with id, name, domain), so a buyer can tell WABC-TV on ABC apart from every other stream the same exchange sells.
  • imp.video with plcmt = 1 (Instream): sound on by default, the video content is what the user came for. See placement vs plcmt for why the older placement field 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):

FieldTypeMeaning
podidstringIdentifier for the pod. Impressions sharing the same podid in one request belong to the same ad pod.
podseqinteger, default 0Position of the pod within the content stream. AdCOM Pod Sequence: -1 last pod, 0 any pod, 1 first pod.
slotinpodinteger, default 0Slot 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.
rqddursinteger arrayExact acceptable creative durations in seconds, for live TV where imprecise durations cause dead air. Mutually exclusive with minduration and maxduration.
poddurinteger, recommendedTotal seconds advertisers may fill in a dynamic pod (the whole break), as opposed to per-slot duration constraints.
maxseqinteger, recommendedMaximum number of ads that may be served into a dynamic pod.
mincpmpersecfloatPrice 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.