Skip to main content

Implementing Banners

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

Requesting a banner​

Use .andBanner(width, height) on an impression to ask for a banner, sized as an aspect ratio (4x1, 8x1, etc.). A request typically covers more than the banner slot alone — here, a banner alongside a product carousel:

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

const request = ads.startRequest('your-zone-id')
.withImp('banner-zone-id')
.andBanner(4, 1)
.withImp('carousel-zone-id')
.andProductCards(ads.defaultNative())
.build();

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

You can ask for more than one banner size on the same impression — the bidder returns whichever it can fill:

const request = ads.startRequest('your-zone-id')
.withImp('banner-zone-id')
.andBanner(4, 1)
.andBanner(8, 1)
.withImp('carousel-zone-id')
.andProductCards(ads.defaultNative())
.build();

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

const request = ads.startRequest('your-zone-id')
.withSurfCategory(['edibles'])
.withKeywords(['organic', 'sale'])
.withImp('banner-zone-id')
.andBanner(4, 1)
.withImp('carousel-zone-id')
.andProductCards(ads.defaultNative())
.build();

The zone ID identifies which ad placement to fill. You can find your zone IDs in the Surfside dashboard. The width and height should match the dimensions of the placement.

Building banner responses​

buildAllBannersCore takes the full bid response and returns a Result containing an array of built banners, pulling out just the banner creatives even if the request also asked for other creative types. Each banner includes an adm field — the HTML of the creative — and an optional clickthrough field if the creative uses a commerce media clickthrough URL.

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

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

Writing the output​

banner.adm is the HTML string for the creative. Write it to the response however your server framework expects:

for (const banner of bannersRes.value) {
res.write(banner.adm);
}

Win notification​

Each banner 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 is important that it fires before the ad is rendered, not after.

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

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

Alternatively, fire it with a fetch call from the client side immediately before inserting the ad into the page:

fetch(banner.win);

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

Viewability tracking​

Each banner also has a viewable field — a URL that must be called when the creative meets viewability (50% viewable for 1 second). 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 creative's container, rather than embedded as a static pixel:

// Adapt this for your framework
let timer;
let hasFired = false;

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

observer.observe(bannerElement);

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.