Sponsored Product / Offer (Single Tile)

A sponsored tile placed within product listing grids alongside organic products

Sponsored Product / Offer (Single Tile)

A single sponsored tile placed within a product listing grid alongside organic products. Behavior depends on the redirectToOffers flag from the ad server.

How It Works

Two flows, one format. The ad server response includes a redirectToOffers flag that determines how the tile behaves:

  • redirectToOffers === falseSponsored Product: Tile shows a product. Click leads to the product page on the shop.
  • redirectToOffers === trueSponsored Offers: Tile shows a product with a CTA button linking to the merchant's shop. Click on the button leads to the merchant's shop.

Both flows are hybrid — your shop is always responsible for fetching product data and rendering the tile.

SourceSponsored ProductSponsored Offers
Ad ServerProduct ID, click tracking, DSA infoProduct ID, offer ID, redirect destination (TBD), click tracking, DSA info
Your ShopProduct image, price, name, URL (fetched by product_id)Product image, price, name, merchant shop name (fetched by product_id + offer_id)

Key difference: In Sponsored Product, the entire tile links to the product page on your shop. In Sponsored Offers, the tile includes a CTA button linking to the merchant's shop. The exact field carrying the redirect URL in the Offers flow is to be agreed upon with Ring DAS.

Data flow:

Sponsored Product (redirectToOffers === false):

  1. Your page requests an ad from Ring DAS
  2. Ad server returns offers[] with product_id and redirectToOffers: false
  3. Your shop fetches product details (image, price, name, URL) from your catalog using product_id
  4. Your shop renders the tile linking to the product page
  5. On click, a tracking pixel fires via the click tracking URL

Sponsored Offers (redirectToOffers === true):

  1. Your page requests an ad from Ring DAS
  2. Ad server returns offers[] with product_id, offer_id, and redirectToOffers: true
  3. Your shop fetches product details (image, price, name, merchant shop name) from your catalog using product_id + offer_id
  4. Your shop renders the tile with a CTA button showing the merchant's shop name
  5. On CTA click, a tracking pixel fires via offer_url and user is redirected to the merchant's shop

Integration Flow Diagram

sequenceDiagram
    participant User as Shop User
    participant Page as Shop Page
    participant ShopAPI as Shop API/Database
    participant DAS as Ring DAS

    User->>Page: 1. Request listing page
    Page->>DAS: 2. Fetch Ad (fetchNativeAd / bidder)
    DAS-->>Page: Response with redirectToOffers flag

    alt redirectToOffers === false (Sponsored Product)
        Note over Page: 3. Extract product_id from response
        Page->>ShopAPI: 4. Fetch product details by product_id
        ShopAPI-->>Page: Product image, price, name, URL
        Note over Page: 5. Render tile linking to product page
    else redirectToOffers === true (Sponsored Offers)
        Note over Page: 3. Extract product_id, offer_id from response
        Page->>ShopAPI: 4. Fetch product details by product_id + offer_id
        ShopAPI-->>Page: Product image, price, name, shop name
        Note over Page: 5. Render tile with CTA button
    end

    Page->>User: 6. Display sponsored tile in product grid
    Note over Page: Fire impression tracking

    User->>Page: 7. Click tile / CTA button
    Page->>DAS: Fire click tracking

Live Demo

See the working implementation:


Frontend Integration (CSR)

Step 1: Load the SDK

Configure the SDK with your Ring DAS credentials. You will receive these values from your Ring DAS account manager.

ParameterDescription
targetSite and area identifier (see details below)
tidYour Ring DAS tenant ID (format: EA-XXXXXXX)
dsainfoSet to 1 to enable DSA (Digital Services Act) transparency info in responses

Target format: site/area

  • site - Your website identifier assigned by Ring DAS, UPPERCASE (e.g., DEMO_PAGE)
  • area - Page type context where the ad appears, UPPERCASE: HOMEPAGE, PRODUCT_DETAIL, LISTING, SEARCH
  • Mobile prefix - For mobile pages, add M_ prefix to site (e.g., M_DEMO_PAGE/LISTING)

Examples:

  • Desktop listing: DEMO_PAGE/LISTING
  • Mobile search: M_DEMO_PAGE/SEARCH
  • Desktop category: DEMO_PAGE/LISTING
<script>
    dlApi = {
        target: "DEMO_PAGE/LISTING", // Format: SITE/AREA. For mobile: M_SITE/AREA
        tid: 'EA-<<NETWORK_ID>>',        // your tenant ID - provided by Ring DAS
        dsainfo: 1,               // enable DSA transparency info
        cmd: []                   // command queue
    };
</script>
<!-- SDK URL - provided by your Ring DAS account manager -->
<script src="https://<<DAS_CDN>>/<<NETWORK_ID>>/build/dlApi/minit.boot.min.js" async></script>

The SDK script URL (minit.boot.min.js) may differ depending on your integration. Your Ring DAS account manager will provide the correct URL for your setup.

Step 2: Configure Consent

As soon as possible after embedding the script, you should pass consent information. This determines whether personalized ads are served and controls the script's behavior.

dlApi.cmd.push(function (dlApi) {
    dlApi.consent({npa: 0});  // 0 = consent given, 1 = no consent
});
ParameterValueDescription
npa0Consent given - personalized ads enabled
npa1No consent - non-personalized ads only

Step 3: Set Targeting Context

Set product and category context BEFORE fetching ads. This helps the ad server select the most relevant campaigns for your page content.

Key-ValueDescription
offer_idsProduct IDs currently displayed on the page
category_idsCategory IDs for the current page context
main_category_idMain product category ID - primary category used for ad matching (product may belong to multiple categories, but has one main)
dlApi.cmd.push(function (dlApi) {
    dlApi.addKeyValue('offer_ids', [10, 20, 1234]);           // offers IDs from current page
    dlApi.addKeyValue('category_ids', [101, 205]);            // category IDs for targeting
    dlApi.addKeyValue('main_category_id', 101);               // main product category ID
});

Why this matters: The ad server uses this context to select campaigns relevant to what the user is viewing, improving click-through rates and campaign performance. See Set Targeting Context for detailed explanation.

Step 4: Fetch the Ad

Use fetchNativeAd() to request a single sponsored tile. The ad server returns offer data and a redirectToOffers flag that determines the tile behavior.

ParameterDescription
slotFixed value sponsored for this format
divContainer element ID - required for viewability measurement
tplCodeFixed value 1746213/Sponsored-Product for Sponsored Single Tile
asyncRenderSet to true to manually control when impression is counted
opts.posSlot position (1, 2, ...) when using multiple slots on the same page
opts.offers_limitOptional. Maximum number of offers returned by the ad server. Request more than one to increase the chance of matching a product in your catalog

Multiple offers for higher fill rate: When offers_limit is greater than 1, the ad server may return multiple offers. Your shop should iterate through the offers and display the first one that matches a product in your catalog. This increases fill rate when some advertised products are not available in your shop.

dlApi.cmd.push(function (dlApi) {
    dlApi.fetchNativeAd({
        slot: 'sponsored',                             // fixed value for Sponsored Single Tile
        div: 'ad-sponsored',                           // container ID for viewability tracking
        opts: {
            pos: 1,                                    // slot position (1, 2, ... for multiple slots)
            offers_limit: 3                            // request up to 3 offers to increase fill rate
        },
        tplCode: '1746213/Sponsored-Product',  // fixed value for Sponsored Single Tile
        asyncRender: true,                             // manually count impression with ad.render()
    }).then(async function (ad) {
        if (!ad) {
            console.log('[Sponsored Tile] No ad available');
            return;
        }

        console.log('[Sponsored Tile] Ad response:', ad);

        // 1. Extract offers from the feed
        var offers = (ad.fields?.feed?.offers) || [];
        if (offers.length === 0) {
            console.log('[Sponsored Tile] No offers in response');
            return;
        }

        var dsa = ad.dsa || null;
        var redirectToOffers = ad.fields?.feed?.redirectToOffers;

        // 2. Find the first offer available in your shop catalog
        var offer = null;
        var shopProduct = null;

        for (var i = 0; i < offers.length; i++) {
            var candidate = offers[i];
            var product = redirectToOffers
                ? await fetchProductDetailsFromShop(candidate.product_id, candidate.offer_id)
                : await fetchProductDetailsFromShop(candidate.product_id);
            if (product) {
                offer = candidate;
                shopProduct = product;
                break;
            }
        }
        if (!offer) {
            console.log('[Sponsored Tile] No matching product found in shop catalog');
            return;
        }

        // 3. Determine the flow based on redirectToOffers flag

        if (redirectToOffers === false) {
            // SPONSORED PRODUCT: tile links to product page on your shop
            renderSponsoredProduct(offer, shopProduct, dsa);
        } else {
            // SPONSORED OFFERS: tile with CTA button linking to merchant's shop
            renderSponsoredOffer(offer, shopProduct, dsa);
        }

        // 4. Count impression after rendering
        ad.render();

    }).catch(function (err) {
        console.error('[Sponsored Tile] Ad could not be loaded:', err);
    });

    // Trigger ad fetch
    dlApi.fetch();
});

Step 5: Handle Response (Dual Flow)

In both flows, your shop fetches product details from your catalog by product_id. The redirectToOffers flag determines how the tile behaves:

  1. Check ad.fields.feed.redirectToOffers from the response
  2. If falseSponsored Product: tile links to the product page on your shop
  3. If trueSponsored Offers: tile shows a CTA button with the merchant shop name linking to the merchant's shop

Step 6: Render the Tile

Sponsored Product (redirectToOffers === false): The entire tile links to the product page on your shop. Click tracking fires via a tracking pixel on click.

/**
 * Render Sponsored Product tile - links to product page on your shop
 */
function renderSponsoredProduct(offer, shopProduct, dsa) {
    var container = document.getElementById('ad-sponsored');

    // Use product data from your shop catalog
    var productUrl = shopProduct.url;
    var productImage = shopProduct.image;
    var productName = shopProduct.name;
    var productPrice = new Intl.NumberFormat('de-DE', {
        style: 'currency',
        currency: 'EUR'
    }).format(shopProduct.price);

    // Build DSA transparency info
    var dsaHtml = '';
    if (dsa) {
        dsaHtml = '<div class="dsa-info">'
            + '<span class="dsa-icon" title="Ad transparency">i</span>'
            + '<div class="dsa-tooltip">'
            + '<div>Advertiser: ' + escapeHtml(dsa.behalf || 'N/A') + '</div>'
            + '<div>Paid by: ' + escapeHtml(dsa.paid || 'N/A') + '</div>'
            + '</div></div>';
    }

    container.innerHTML = ''
        + dsaHtml
        + '<span class="sponsored-badge">Sponsored Product</span>'
        + '<a href="' + escapeHtml(productUrl) + '" target="_blank" rel="noopener nofollow sponsored"'
        + ' onclick="new Image().src=\'' + escapeHtml(offer.offer_url || '') + '\';">'
        + '<img src="' + escapeHtml(productImage) + '" alt="' + escapeHtml(productName) + '">'
        + '<div class="product-name">' + escapeHtml(productName) + '</div>'
        + '<div class="product-price">' + productPrice + '</div>'
        + '</a>';
}

Sponsored Offers (redirectToOffers === true): The tile shows product data from your shop catalog with a CTA button. The CTA button displays the merchant's shop name and links to the merchant's shop.

/**
 * Render Sponsored Offer tile - CTA button links to merchant's shop
 * Product data (image, name, price) comes from your shop catalog
 */
function renderSponsoredOffer(offer, shopProduct, dsa) {
    var container = document.getElementById('ad-sponsored');

    // Get shop name from your shop catalog (fetched alongside product data)
    var shopName = shopProduct.shopName || 'Shop';

    // Use product data from your shop catalog
    var productImage = shopProduct.image;
    var productName = shopProduct.name;
    var productPrice = new Intl.NumberFormat('de-DE', {
        style: 'currency',
        currency: 'EUR'
    }).format(shopProduct.price);

    // Build DSA transparency info
    var dsaHtml = '';
    if (dsa) {
        dsaHtml = '<div class="dsa-info">'
            + '<span class="dsa-icon" title="Ad transparency">i</span>'
            + '<div class="dsa-tooltip">'
            + '<div>Advertiser: ' + escapeHtml(dsa.behalf || 'N/A') + '</div>'
            + '<div>Paid by: ' + escapeHtml(dsa.paid || 'N/A') + '</div>'
            + '</div></div>';
    }

    // Note: The exact redirect URL field for the CTA destination
    // is to be agreed upon with Ring DAS. Currently uses offer.offer_url.
    container.innerHTML = ''
        + dsaHtml
        + '<span class="sponsored-badge">Sponsored Offer</span>'
        + '<img src="' + escapeHtml(productImage) + '" alt="' + escapeHtml(productName) + '">'
        + '<div class="product-name">' + escapeHtml(productName) + '</div>'
        + '<div class="product-price">' + productPrice + '</div>'
        + '<a class="cta-button" href="' + escapeHtml(offer.offer_url) + '" target="_blank" rel="noopener nofollow sponsored"'
        + ' onclick="new Image().src=\'' + escapeHtml(offer.offer_url) + '\';">'
        + escapeHtml(shopName) + '</a>';
}

// Helper function to escape HTML special characters
function escapeHtml(text) {
    if (!text) return '';
    var div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

Implementation note: The ad.render() call after rendering counts the impression. Always call it after the tile is visible in the DOM. Both flows fire offer.offer_url as a tracking pixel on click. In Sponsored Product the tile links to the product page on your shop; in Sponsored Offers the CTA button links to the merchant's shop (exact destination field TBD).


Backend Integration (SSR)

For server-side rendering, fetch ad data from the bidder, then handle the dual flow before rendering the page.

Required: The SDK from Step 1 (Frontend) must still be loaded on the page for viewability tracking via registerBidResponse().

Step 1: Fetch Ad from Bidder

Make a GET request to the Ring DAS bidder endpoint. The bidder URL will be provided by your Ring DAS account manager.

Request Configuration

ParameterDescription
BIDDER_URLBidder endpoint URL - provided by your Ring DAS account manager
NETWORK_IDYour Ring DAS network ID (e.g., 1746213)
tmaxRequest timeout in milliseconds
ext.srcOptional. Set to s2s for server-to-server requests

Placement Parameters

These parameters define WHERE the ad appears and WHICH format to serve.

ParameterDescription
site.idYour site identifier in Ring DAS
site.ext.areaPage type context, UPPERCASE: HOMEPAGE, PRODUCT_DETAIL, LISTING, SEARCH
imp[].tagidFixed value sponsored for this format

Targeting Parameters

ParameterDescription
imp[].ext.offeridsList of product offer IDs from current page for contextual targeting and relevancy (optional)
imp[].ext.categoryidsProduct category IDs for contextual targeting and relevancy (optional)

Consent & Privacy Parameters

ParameterDescription
user.ext.npaConsent flag: false = consent given, true = no consent
regs.gdprGDPR applies flag: 1 = GDPR applies, 0 = does not apply
regs.gppGPP consent string (optional) - TCF-compliant consent string from your CMP
regs.ext.dsaSet to 1 to request DSA transparency info in response

Ad Behavior Parameters

ParameterDescription
imp[].ext.no_redirectSet to true if adclick URLs should only fire tracking pixel without redirect (shop handles redirect via href)
imp[].ext.offers_limitOptional. Maximum number of offers returned by the ad server. Request more than one to increase the chance of matching a product in your catalog

Consent handling: You can pass consent via user.ext.npa (simple flag) OR via regs.gpp (TCF-compliant string). If you have a TCF-compliant CMP, use the GPP string for more granular consent information.

User Identifier

Important for backend integration: Pass a user identifier consistently per user. This enables frequency capping, personalization, and other targeting features. Without an identifier, each request is treated as a new user.

ParameterDescription
user.ext.ids.sidCustom user/session identifier provided by the integrating party. Can be any persistent, unique value your system already maintains (e.g., internal user ID, session token, or device identifier). Free-form string.
user.ext.ids.luLocal user ID from the ea_uuid cookie on your shop's domain (set by Ring DAS). Must follow the alphanumeric format (e.g., 202408221036415499301131).

Choosing an identifier:

  • sid — A free-form string you provide. Pass your own session ID, internal user ID, or any other persistent identifier. The integrating party is responsible for ensuring persistence and consistency per user.
  • lu — The value from the ea_uuid cookie set by Ring DAS on the user's browser. Must follow the strict alphanumeric format.

You can pass both identifiers simultaneously. At least one is recommended.

For more details on user identifiers, see API Parameters Reference.

// Node.js backend
const NETWORK_ID = '${NETWORK_ID}';
const BIDDER_URL = `https://das.idealo.com/${NETWORK_ID}/bid`;

const requestBody = {
    id: Math.random().toString(16).substring(2, 15),
    imp: [{
        id: 'imp-1',
        tagid: 'sponsored',                            // fixed value for Sponsored Single Tile
        secure: 1,
        native: { request: "{}" },
        ext: {
            no_redirect: true,                         // adclick URLs fire tracking only, no redirect
            offers_limit: 3,                           // request up to 3 offers to increase fill rate
            offerids: ['12345', '67890'],              // optional: product IDs from current page
            categoryids: ['101', '205']                // optional: category IDs for targeting
        }
    }],
    site: {
        id: 'DEMO_PAGE',                               // your site ID (UPPERCASE)
        ext: {
            area: 'LISTING'                            // page type (UPPERCASE): HOMEPAGE, PRODUCT_DETAIL, LISTING, SEARCH
        }
    },
    user: {
        ext: {
            npa: false,                                // false = consent given, true = no consent
            ids: {
                lu: '202408221036415499301131',        // from ea_uuid cookie
                sid: 'your-persistent-user-or-session-id'   // your own identifier (alternative to lu)
            }
        }
    },
    ext: {
        network: NETWORK_ID,
        keyvalues: {},                                 // optional targeting parameters
        is_non_prebid_request: true,
        src: 's2s'                                     // optional: indicates server-to-server request
    },
    regs: {
        gdpr: 1,                                       // 1 = GDPR applies, 0 = does not apply
        gpp: 'YOUR_GPP_STRING',                        // optional: TCF-compliant consent string from CMP
        ext: {
            dsa: 1                                     // request DSA transparency info
        }
    },
    tmax: 1000                                         // request timeout in milliseconds
};

// Send as GET request with body in data= parameter
const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const response = await fetch(`${BIDDER_URL}?data=${encodedData}`);

// Check for no ad available (204 No Content)
if (response.status === 204) {
    console.log('No ad available');
    return null;
}

const bidResponse = await response.json();

No ad available: When no ad matches the request criteria, the server returns HTTP 204 No Content with an empty response body. Always check the status code before parsing JSON.

Step 2: Parse Response and Handle Dual Flow

Extract the offer data and redirectToOffers flag, then handle each flow:

const bid = bidResponse?.seatbid?.[0]?.bid?.[0];
if (!bid) {
    return null;  // No bid in response
}

// Parse adm (it's a JSON string)
const adm = JSON.parse(bid.adm);

// 1. Extract offer data and redirectToOffers flag
const offers = adm.fields?.feed?.offers || [];
const redirectToOffers = adm.fields?.feed?.redirectToOffers;

if (offers.length === 0) {
    return null;  // No offers in response
}

// 2. Extract DSA transparency info
const dsaInfo = bid.ext?.dsa ? {
    advertiser: bid.ext.dsa.behalf,
    payer: bid.ext.dsa.paid
} : null;

// 3. Find the first offer available in your shop catalog
let offer = null;
let shopData = null;

for (const candidate of offers) {
    const product = redirectToOffers
        ? await fetchProductFromDatabase(candidate.product_id, candidate.offer_id)
        : await fetchProductFromDatabase(candidate.product_id);
    if (product) {
        offer = candidate;
        shopData = product;
        break;
    }
}
if (!offer) {
    console.log('No matching product found in shop catalog');
    return null;
}

// 4. Determine the flow based on redirectToOffers flag
if (redirectToOffers === false) {
    console.log('Sponsored Product flow - product:', offer.product_id);
} else {
    console.log('Sponsored Offers flow - product:', offer.product_id);
}

// 5. Prepare data for rendering
const adData = {
    offer,
    redirectToOffers,
    shopData,                 // product data from your shop catalog
    dsaInfo,
    rawResponse: bidResponse,
    containerId: 'ad-sponsored'
};

Step 3: Render HTML + Register Response

Render the tile based on the flow, then register the bid response for viewability tracking:

<div id="ad-sponsored">

    <% if (adData.redirectToOffers === false) { %>
    <!-- SPONSORED PRODUCT: tile links to product page on your shop -->
    <div class="tile">
        <% if (adData.dsaInfo) { %>
        <div class="dsa-info">
            <span>Advertiser: <%= adData.dsaInfo.advertiser %></span>
            <span>Paid by: <%= adData.dsaInfo.payer %></span>
        </div>
        <% } %>
        <span class="sponsored-badge">Sponsored Product</span>
        <a href="<%= adData.shopData.url %>"
           target="_blank" rel="noopener nofollow sponsored"
           onclick="new Image().src='<%= adData.offer.offer_url %>';">
            <img src="<%= adData.shopData.image %>" alt="<%= adData.shopData.name %>">
            <div class="product-name"><%= adData.shopData.name %></div>
            <div class="product-price">
                <%= new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(adData.shopData.price) %>
            </div>
        </a>
    </div>

    <% } else { %>
    <!-- SPONSORED OFFERS: product data from shop catalog + CTA button links to merchant's shop -->
    <div class="tile">
        <% if (adData.dsaInfo) { %>
        <div class="dsa-info">
            <span>Advertiser: <%= adData.dsaInfo.advertiser %></span>
            <span>Paid by: <%= adData.dsaInfo.payer %></span>
        </div>
        <% } %>
        <span class="sponsored-badge">Sponsored Offer</span>
        <img src="<%= adData.shopData.image %>" alt="<%= adData.shopData.name %>">
        <div class="product-name"><%= adData.shopData.name %></div>
        <div class="product-price">
            <%= new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(adData.shopData.price) %>
        </div>
        <!-- Note: The redirect destination href field is TBD — to be agreed upon with Ring DAS -->
        <a class="cta-button" href="<%= adData.offer.offer_url %>"
           target="_blank" rel="noopener nofollow sponsored"
           onclick="new Image().src='<%= adData.offer.offer_url %>';">
            <%= adData.shopData.shopName || 'Shop' %>
        </a>
    </div>
    <% } %>

</div>

<!-- Register for viewability and impression tracking -->
<script>
    dlApi.cmd.push(function () {
        dlApi.registerBidResponse(<%- JSON.stringify(adData.rawResponse) %>, 'ad-sponsored');
    });
</script>
⚠️

registerBidResponse() counts impressions. Only call it when the ad is actually rendered and visible to the user. If your shop decides not to display the ad (e.g., product not in catalog, business logic prevents display), do NOT call registerBidResponse() — calling it without a visible ad counts a false impression.


What the Ad Server Returns

FieldDescription
ad.fields.feed.redirectToOffersDetermines the flow: false = Sponsored Product, true = Sponsored Offers
ad.fields.feed.offersArray of offer objects
ad.fields.feed.offers[].product_idProduct ID — use to fetch product details from your shop catalog
ad.fields.feed.offers[].offer_idOffer ID — used with product_id to fetch product details in the Sponsored Offers flow
ad.fields.feed.offers[].offer_urlClick tracking URL — fire as pixel on click
ad.dsa.behalfAdvertiser name (DSA compliance, requires dsainfo: 1)
ad.dsa.paidEntity that paid for the ad (DSA compliance)
ad.meta.adclickClick tracking base URL — available in response but not used in these examples

Sponsored Offers redirect destination: The exact field carrying the redirect URL for the CTA button in the Sponsored Offers flow is to be agreed upon with Ring DAS. Currently, offer_url is used for click tracking and may serve as the redirect destination. Confirm the field mapping with your Ring DAS account manager.


Testing Your Integration

Implement this to enable integration testing without running production campaigns. Pass URL parameters from the page URL to your backend request.

Backend (Bidder API):

// Extract test parameters from page URL
const pageUrl = 'https://your-shop.com/?test_kwrd=test+keywords#adbeta=l1234567!slot.sponsored';
const url = new URL(pageUrl);
const adbeta = url.hash.match(/adbeta=([^&]+)/)?.[1];
const testKwrd = url.searchParams.get('test_kwrd');

const requestBody = {
    // ... standard request fields ...
    site: {
        id: 'DEMO_PAGE',
        ext: {
            area: 'LISTING',
            ...(testKwrd && { kwrd: testKwrd })     // if test_kwrd in URL, add to kwrd
        }
    },
    ext: {
        network: NETWORK_ID,
        keyvalues: {},
        is_non_prebid_request: true,
        ...(adbeta && { adbeta })                   // if adbeta in URL, add it
    },
    // ...
};

Caching Considerations

🚧

Important: Improper caching can significantly reduce ad revenue and break tracking.

Do NOT cache:

  • Tracking pixels - Caching impression/click pixels prevents accurate counting and drastically reduces revenue
  • API responses - Caching bid responses prevents real-time optimization, budget pacing, and frequency capping

Safe to cache:

  • Static assets (CSS, JS, images from your shop)
  • Product data from your own database

Multiple Ads on Single Page

To display multiple sponsored tiles on a single page (e.g., positions 1 and 4 in a product grid), include multiple impressions in one request with unique IDs and positions.

Backend (Bidder API):

const requestBody = {
    id: 'request-123',
    imp: [
        {
            id: 'ad-sponsored-1',                       // unique ID for first slot
            tagid: 'sponsored',
            secure: 1,
            native: { request: "{}" },
            ext: { pos: 1 }                            // position in the grid
        },
        {
            id: 'ad-sponsored-2',                       // unique ID for second slot
            tagid: 'sponsored',
            secure: 1,
            native: { request: "{}" },
            ext: { pos: 2 }                            // position in the grid
        }
    ],
    // ... rest of request unchanged
};

Response handling:

Each impression may return a separate bid. Match bids to impressions using the impid field:

const bids = bidResponse?.seatbid?.[0]?.bid || [];
bids.forEach(bid => {
    const impressionId = bid.impid;  // matches imp[].id from request
    // Parse each bid independently - each may have different redirectToOffers value
    const adm = JSON.parse(bid.adm);
    const redirectToOffers = adm.fields?.feed?.redirectToOffers;

    // Handle each slot's dual flow independently
});
⚠️

Per-slot registerBidResponse() rules:

  • Each slot resolves independently — one may be a Sponsored Product while another is a Sponsored Offer.
  • Call per element: Each slot requires a separate registerBidResponse() call with its own container element ID.
  • Filter bids per slot: Pass a response containing ONLY the bid displayed in that container — filter the seatbid[].bid array to include only the matching bid. Do not pass the entire response with all bids.
  • Call only when displayed: Only call registerBidResponse() after the ad is actually rendered. If no matching product is found and the slot stays empty, skip the call — it counts impressions.

Frontend (CSR):

For frontend integration, make separate fetchNativeAd() calls with different opts.pos values:

// Slot 1 (position 1 in the grid)
dlApi.fetchNativeAd({
    slot: 'sponsored',
    div: 'ad-sponsored-1',
    opts: { pos: 1 },
    tplCode: '1746213/Sponsored-Product',
    asyncRender: true
}).then(async function (ad) {
    // Handle ad response for slot 1
});

// Slot 2 (position 4 in the grid)
dlApi.fetchNativeAd({
    slot: 'sponsored',
    div: 'ad-sponsored-2',
    opts: { pos: 2 },
    tplCode: '1746213/Sponsored-Product',
    asyncRender: true
}).then(async function (ad) {
    // Handle ad response for slot 2
});

dlApi.fetch();

Related