Register Bid Response

Viewability tracking and impression counting via dlApi.registerBidResponse() — applies to all Ring DAS SSR ad formats

Register Bid Response

dlApi.registerBidResponse() is the SDK call that activates viewability tracking and counts an ad impression. It must be called in the browser after the ad is rendered — not on the server side.

dlApi.cmd.push(function (dlApi) {
    dlApi.registerBidResponse(adm, 'ad-container-id');
});

Applies to all backend (SSR) formats: for example: Sponsored Single Tile, Sponsored Product Slider, Brand Store, Branded Products. The rules on this page apply identically to all formats.

⚠️

The DL API SDK must be loaded on the page for this to work. dlApi.cmd.push() queues the call until the SDK is ready — do not call dlApi.registerBidResponse() directly outside of dlApi.cmd.push().


Which adm to pass

Extract the adm for the rendered slot and pass it directly to registerBidResponse().

Single slot (most formats):

// Extract adm for the rendered slot — inject this into the page
const adm = bidResponse.seatbid[0].bid[0].adm;

adm is the ad markup string returned by the bidder. It may be a JSON string or a pre-parsed object — the SDK accepts both.

Multi-slot — find the bid by impid:

// For multi-slot: find the bid matching the rendered slot's impid
const adm = bidResponse.seatbid
    .flatMap(seat => seat.bid)
    .find(b => b.impid === renderedImpid)?.adm;

See API reference for the required adm payload structure.


Filter offers to match displayed products

The adm payload may contain more offers in adm.fields.feed.offers than your page actually renders. For example, the bidder may return 3 offers (via offers_limit), but your page displays only 1. Before calling registerBidResponse(), filter the offers array so it contains only the offers that were actually rendered.

⚠️

registerBidResponse() fires tracking events for every offer in adm.fields.feed.offers. Passing unrendered offers inflates impression metrics — the campaign reports will count impressions for products the user never saw.

Single displayed offer (most common):

// adm from the bid response — parse it if it's a JSON string
const adm = typeof bid.adm === 'string' ? JSON.parse(bid.adm) : bid.adm;
const offers = adm.fields?.feed?.offers || [];

// Your shop picks one offer to display (e.g. the first available product)
const displayedOffer = offers.find(offer =>
    isProductAvailableInShop(offer.product_id)
);

if (!displayedOffer) {
    // No matching offer found — do not render, do not call registerBidResponse
    return;
}

// Keep only the displayed offer
adm.fields.feed.offers = [displayedOffer];

// Render the tile...
renderTile(displayedOffer);

// Pass the filtered adm — the SDK accepts a parsed object
dlApi.cmd.push(function (dlApi) {
    dlApi.registerBidResponse(adm, 'ad-container-id');
});

Subset of displayed offers (e.g. carousel showing 3 out of 5):

const adm = typeof bid.adm === 'string' ? JSON.parse(bid.adm) : bid.adm;
const allOffers = adm.fields?.feed?.offers || [];

// Your page displays at most 3 products
const MAX_DISPLAY = 3;
const displayedOffers = allOffers.slice(0, MAX_DISPLAY);

// Keep only the displayed offers
adm.fields.feed.offers = displayedOffers;

// Render products...
renderProducts(displayedOffers);

// Pass the filtered adm
dlApi.cmd.push(function (dlApi) {
    dlApi.registerBidResponse(adm, 'ad-branded-products');
});

The SDK accepts adm as a JSON string or a pre-parsed object. After filtering, you can pass the modified object directly — no need to re-stringify.


What it does

When called, registerBidResponse() instructs the DL API SDK to:

  • Start observing the ad container for viewability (using IntersectionObserver)
  • Fire an impression event
  • Count the impression and viewable impression in Ring DAS campaign reporting

When to call it

Call registerBidResponse() only when the ad tile is actually rendered.

ScenarioCall registerBidResponse()?
Ad fetched and renderedYes
Ad fetched but ad no rendered — slot stays emptyNo
Shop business logic prevents displaying the adNo
Server returned 204 (no ad)No

Calling registerBidResponse() for an empty slot counts a false impression, inflating campaign metrics incorrectly.


Per-slot rules for multi-slot formats

For formats that support multiple ad slots (Sponsored Product Slider, Sponsored Single Tile with multiple positions):

RuleDescription
One call per slotEach rendered slot requires its own registerBidResponse() call with its own container element ID
Pass the adm for that slotExtract and pass only the adm for the specific rendered slot
Skip unrendered slotsDo not call registerBidResponse() for slots that were not rendered — see When to call it

The server injects an adm per slot into the page (or null if the slot was not rendered). The browser calls registerBidResponse() only for slots that were actually rendered:

// admSlot1/2/3: adm strings injected by the server — null if slot was not rendered
dlApi.cmd.push(function (dlApi) {
    if (admSlot1) { dlApi.registerBidResponse(admSlot1, 'ad-sponsored-1'); }
    if (admSlot2) { dlApi.registerBidResponse(admSlot2, 'ad-sponsored-2'); }
    if (admSlot3) { dlApi.registerBidResponse(admSlot3, 'ad-sponsored-3'); }
});
⚠️

Passing null or an empty object is not a safe no-op — skip the call entirely for unrendered slots.


SSR pattern

The server resolves the ad, renders the HTML, and injects the bid data into the page. The browser SDK then picks it up after load.

Server side (Node.js / EJS example):

// After fetching and resolving the bid response, inject data into the page
// adm: the ad markup for this specific slot
// containerId: the HTML element ID of the rendered ad container
const slotData = {
    adm: bidResponse.seatbid[0].bid[0].adm,
    containerId: 'ad-product-slider-slot-1',
};
<script>
  // Injected by server
  const adSlotData = <%- JSON.stringify(slotData) %>;
</script>

Browser side (runs after SDK loads):

dlApi.cmd.push(function (dlApi) {
    dlApi.registerBidResponse(adSlotData.adm, adSlotData.containerId);
});

Multi-slot pattern:

// adSlotsData: array of { adm, containerId } injected by the server
// adm is null for slots that were not rendered
dlApi.cmd.push(function (dlApi) {
    adSlotsData.forEach(function (slot) {
        if (slot.adm) {
            dlApi.registerBidResponse(slot.adm, slot.containerId);
        }
    });
});

API reference

dlApi.registerBidResponse(adm, containerId)
ParameterTypeDescription
admstring|objectThe adm value from the bid — JSON string or pre-parsed object
containerIdstringThe HTML id attribute of the element containing the rendered ad

adm payload

The adm may be passed as a JSON string or an already-parsed object — the SDK accepts both. All other fields from the original bidder adm are optional — if payload size is a concern, strip them server-side before injecting into the page.

{
  adserver_meta: {   // Required — pass whole object
    ...
  },
  meta: {            // Required — pass whole object
    ...
  },
  gctx: string,      // Required
  lctx: string       // Required
}

Related