Skip to main content

Implementing Videos

This section covers the request options and response fields specific to video creatives. For how to build and send a bid request, see Building Bid Requests.

Requesting a video​

Use .andVideo(width, height, minDuration, maxDuration) on an impression to ask for a video. Duration values are in seconds. A request typically covers more than the video slot alone — here, a video alongside a banner:

/* const ads = await LoadSurfAdsCoreHeadless({ ... }) — see Initialization */

const request = ads.startRequest('your-zone-id')
.withImp('video-zone-id')
.andVideo(1920, 1080, 0, 30)
.withImp('banner-zone-id')
.andBanner(4, 1)
.build();

const bidsRes = await ads.requestBids(request);
if (!bidsRes.ok) {
console.error(bidsRes.error);
return;
}

You can also pass categories and keywords for targeting before calling .withImp():

const request = ads.startRequest('your-zone-id')
.withSurfCategory(['grocery', 'beverages'])
.withKeywords(['organic', 'sale'])
.withImp('video-zone-id')
.andVideo(1920, 1080, 0, 30)
.withImp('banner-zone-id')
.andBanner(4, 1)
.build();

Building video responses​

buildAllVideosCore takes the full bid response and returns a Result containing an array of built videos, pulling out just the video creatives even if the request also asked for other creative types. Each video includes a video field containing the VAST XML string, and a clickthrough URL.

const videosRes = ads.buildAllVideosCore(bidsRes.value);
if (!videosRes.ok) {
console.error(videosRes.error);
return;
}

If the request mixed creative types on the same impression, use buildAllResponsesCore instead (see Building Bid Requests) and switch on creative.type === 'video'.

Writing the output​

video.video is the VAST XML string. Write it to the response for your video player to consume:

for (const video of videosRes.value) {
res.write(video.video);
}

Win notification​

Each video also has a win field — a URL that must be called immediately before the creative renders on the page. This is how Surfside tracks impressions, and it should fire right before the video is rendered, not when the server generates the response and not after the player has already rendered.

The simplest approach is to embed it as a 1×1 tracking pixel alongside the player:

for (const video of videosRes.value) {
res.write(`${yourRenderVideoFunction(video.video)}<img src="${video.win}" width="1" height="1" style="display:none">`);
}

Alternatively, fire it with a fetch call from the client side immediately before the player is inserted:

fetch(video.win);

There is no meaningful response, so you do not need to await it or handle errors.

Viewability tracking​

Each video also has a viewable field — a URL that must be called when the creative meets viewability (50% viewable for 2 seconds). This is separate from the win notification: win confirms the ad was served and rendered, while viewable confirms it was actually seen by the user.

Because viewability depends on scroll position and dwell time, it's typically fired from an IntersectionObserver watching the rendered player's container, rather than embedded as a static pixel. The Headless workflow doesn't render the player for you, and there's no way to predict how any given third-party video player structures its DOM — adapt the selector below to whatever wrapper element your player renders into:

// Adapt this for your framework and video player
const videoElement = document.querySelector('#some-video-player');

let timer;
let hasFired = false;

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5 && !hasFired) {
timer = setTimeout(() => {
navigator.sendBeacon(video.viewable);
hasFired = true;
}, 2_000); // dwell time, 2 seconds
} else {
clearTimeout(timer);
timer = undefined;
}
});
}, { threshold: 0.5 }); // 50% viewable

observer.observe(videoElement);

navigator.sendBeacon is preferable to fetch here since it's more likely to complete even if the user navigates away right as the timer fires. As with win, there is no meaningful response, so you do not need to handle errors.