Sponsored Product / Offer (Product Slider)

A horizontal product slider combining organic tiles with sponsored tiles served by Ring DAS

Sponsored Product / Offer (Product Slider)

A horizontal product slider that places sponsored tiles alongside organic product cards in a scrollable carousel. Sponsored positions are filled by Ring DAS; organic content is managed by your shop. The format uses the same dual-flow model as Sponsored Single Tile — the redirectToOffers flag determines whether a tile drives traffic to the product page (Sponsored Product) or to a merchant's shop offer (Sponsored Offers).

How It Works

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

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

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

SourceSponsored ProductSponsored Offers
Ad ServerProduct ID, click tracking, DSA infoProduct ID, offer ID, redirect destination, 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 from Single Tile: The slider uses tagid: product-tile-slider / product-tile-slider2 instead of product-tile, and the page context (site.ext.area) is set to match the page where the slider appears. The excluded_offers_ids field is important when organic products are shown alongside the slider — use it to prevent the same product appearing both organically and as a sponsored tile in the same view.

Integration Flow Diagram

sequenceDiagram
    participant User as Shop User
    participant Page as Shop Page (SSR)
    participant ShopAPI as Shop Catalog/DB
    participant DAS as Ring DAS Bidder

    User->>Page: 1. Request page with product slider
    Page->>DAS: 2. Fetch ads (product-tile-slider + product-tile-slider2, multiply imps, excluded_offers_ids)
    DAS-->>Page: Response — seatbid array with bids per impression

    loop For each slider slot
        Note over Page: 3. Iterate seatbid — match impid to slot group
        Page->>ShopAPI: 4. Lookup product/offer in catalog (product_id or product_id + offer_id)
        ShopAPI-->>Page: Product image, price, name, URL / shop name
    end

    Page->>User: 5. Render page with slider — sponsored tiles mixed with organic cards
    Note over Page: Fire impression via registerBidResponse() per slot

    User->>Page: 6. Click tile / CTA button
    Page->>DAS: Fire adclick tracking pixel

Backend Integration (SSR)

For server-side rendering, fetch ad data from the bidder, resolve each slider slot against your catalog, then render the slider before serving the page.

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

Step 1: Fetch from Bidder

Make a GET request to the Ring DAS bidder. Define one impression group per slider slot. Use multiple impression replicas (same tagid, different id) per slot to increase the number of ad candidates returned.

Placement Parameters

ParameterDescription
site.idYour site identifier in Ring DAS
site.ext.areaPage type context where the slider appears, UPPERCASE (e.g., OOP for Offers of Product pages). Provided by your Ring DAS account manager
imp[].tagidproduct-tile-slider for first/best slider position; product-tile-slider2 for additional positions (requires imp[].ext.pos)
imp[].ext.posPosition index — required only for product-tile-slider2 (e.g., 1 for second slot, 2 for third)

Targeting Parameters

ParameterDescription
ext.keyvalues.main_category_idMain category ID of the product currently viewed — primary signal for ad matching
ext.categoryidsCategory IDs for contextual targeting — applies globally to all impressions in the request (optional)
ext.excluded_offers_idsList of offer IDs already shown as organic content on the same page — prevents the same offer appearing both organically and as a sponsored tile (optional)

excluded_offers_ids prevents duplicate content. When the page shows organic product listings alongside the slider, pass the IDs of those organic items here. If a sponsored tile would promote a product already visible organically, the ad server excludes it — avoiding duplicates and keeping the page experience clean.

Multiply impressions for higher fill rate: Replicate the same slot as multiple entries in the imp array — each with a unique id but the same tagid. The bidder returns a bid for each matching impression, giving more offer candidates to match against your catalog. Your resolution logic then picks the first catalog match across all replicas for that slot.

// Slot 1 — product-tile-slider, 3 replicas
{ id: 'imp-1-0', tagid: 'product-tile-slider', ... },
{ id: 'imp-1-1', tagid: 'product-tile-slider', ... },
{ id: 'imp-1-2', tagid: 'product-tile-slider', ... },

// Slot 2 — product-tile-slider2, 3 replicas
{ id: 'imp-2-0', tagid: 'product-tile-slider2', ext: { pos: 1 }, ... },
{ id: 'imp-2-1', tagid: 'product-tile-slider2', ext: { pos: 1 }, ... },
{ id: 'imp-2-2', tagid: 'product-tile-slider2', ext: { pos: 1 }, ... },

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 so adclick URLs fire tracking pixel only (shop handles redirect via href)
imp[].ext.offers_limitMaximum number of offers returned per impression. Request more than one to increase the chance of a catalog match
// 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: [
        // Each slot is replicated ×3 (same tagid, unique ids) to increase the number of ad candidates
        // Slot 1 — product-tile-slider (first/best position)
        { id: 'imp-1-0', tagid: 'product-tile-slider', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } },
        { id: 'imp-1-1', tagid: 'product-tile-slider', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } },
        { id: 'imp-1-2', tagid: 'product-tile-slider', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } },
        // Slot 2 — product-tile-slider2 (additional positions), pos required; 3 replicas
        { id: 'imp-2-0', tagid: 'product-tile-slider2', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3, pos: 1 } },
        { id: 'imp-2-1', tagid: 'product-tile-slider2', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3, pos: 1 } },
        { id: 'imp-2-2', tagid: 'product-tile-slider2', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3, pos: 1 } },
    ],
    site: {
        id: 'DEMO_PAGE',                              // your site ID (UPPERCASE)
        page: 'https://your-shop.com/page-url',      // full URL of the page containing the slider
        ext: {
            area: 'OOP'                               // page type context — provided by Ring DAS account manager
        }
    },
    user: {
        ext: {
            npa: false,                               // false = consent given, true = no consent
            ids: {
                lu: '202408221036415499301131',       // from ea_uuid cookie
                sid: 'your-persistent-user-or-session-id'
            }
        }
    },
    ext: {
        network: NETWORK_ID,
        keyvalues: {
            main_category_id: '101',                  // main category ID of the viewed product
        },
        excluded_offers_ids: ['organic-offer-1', 'organic-offer-2'],  // organic offer IDs from price table
        categoryids: ['101', '205'],                  // optional: category IDs for targeting
        is_non_prebid_request: true,
        src: 's2s'                                    // indicates server-to-server request
    },
    regs: {
        gdpr: 1,
        gpp: 'YOUR_GPP_STRING',                       // optional: TCF-compliant consent string
        ext: { dsa: 1 }
    },
    tmax: 1000
};

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

if (response.status === 204) {
    console.log('No ad available');
    return [];
}

const bidResponse = await response.json();

No ad available: When no ad matches the request, the server returns HTTP 204 No Content. Always check the status code before parsing JSON.

Step 2: Resolve Slots

For each slider slot, iterate through the seatbids to find a bid whose impid belongs to that slot's impression group, then find the first offer that matches your catalog.

// Map impression IDs to slider containers — must match the imp[] ids from the request
const SLOT_CONFIG = [
    { impIds: ['imp-1-0', 'imp-1-1', 'imp-1-2'], containerId: 'ad-slider-1' },
    { impIds: ['imp-2-0', 'imp-2-1', 'imp-2-2'], containerId: 'ad-slider-2' },
];

/**
 * Resolve one slider slot: find the first catalog match across all impression replicas.
 * @param {object} bidResponse - Full bid response from the bidder
 * @param {object} slot - Slot definition from SLOT_CONFIG
 * @returns {object|null} Resolved ad data, or null if no catalog match found
 */
async function resolveSlot(bidResponse, slot) {
    for (const seat of bidResponse.seatbid) {
        for (const bid of seat.bid) {
            // Check if this bid belongs to this slot's impression group
            if (!slot.impIds.includes(bid.impid)) continue;

            const adm = JSON.parse(bid.adm);
            const offers = adm.fields?.feed?.offers || [];
            const redirectToOffers = adm.fields?.feed?.redirectToOffers;
            const dsaInfo = bid.ext?.dsa
                ? { advertiser: bid.ext.dsa.behalf, payer: bid.ext.dsa.paid }
                : null;

            // Find the first offer available in your shop catalog
            for (const offer of offers) {
                const product = redirectToOffers
                    ? await fetchProductFromCatalog(offer.product_id, offer.offer_id)  // Sponsored Offers: needs product_id + offer_id
                    : await fetchProductFromCatalog(offer.product_id);                  // Sponsored Product: product_id only

                if (product) {
                    return {
                        matchedOffer: offer,
                        matchedBidImpid: bid.impid,
                        redirectToOffers,
                        shopProduct: product,
                        dsaInfo,
                        containerId: slot.containerId,
                    };
                }
            }
        }
    }
    console.log(`No catalog match for slot "${slot.containerId}"`);
    return null;
}

// Resolve all slots
const resolvedSlots = [];
for (const slot of SLOT_CONFIG) {
    const result = await resolveSlot(bidResponse, slot);
    if (result) resolvedSlots.push(result);
}

Step 3: Filter Response Per Slot

Before calling registerBidResponse(), filter the raw response to include only the seatbid for the specific slot being rendered. This ensures each call counts exactly one impression.

/**
 * Filter the raw response to only the seatbid containing the given impid.
 * Pass this filtered response to registerBidResponse() — not the full rawResponse.
 */
function filterResponse(rawResponse, impid) {
    return {
        ...rawResponse,
        seatbid: rawResponse.seatbid.filter(seat =>
            seat.bid.some(b => b.impid === impid)
        ),
    };
}

// Attach filtered responses to each resolved slot
resolvedSlots.forEach(slot => {
    slot.filteredResponse = filterResponse(bidResponse, slot.matchedBidImpid);
});

Step 4: Render Tiles

Render each slider tile based on the redirectToOffers flag. Product data (image, price, name, URL) always comes from your shop catalog — the ad server provides only IDs and tracking URLs.

<!-- Slider container — inject server-rendered HTML below -->
<div id="product-slider">

    <% resolvedSlots.forEach(adData => { %>
    <div id="<%= adData.containerId %>" class="slider-card sponsored-card">

        <% if (adData.dsaInfo) { %>
        <div class="dsa-info">
            Advertiser: <%= adData.dsaInfo.advertiser %> &middot; Paid by: <%= adData.dsaInfo.payer %>
        </div>
        <% } %>

        <span class="sponsored-badge">Sponsored</span>

        <% if (adData.redirectToOffers === false) { %>
        <!-- SPONSORED PRODUCT: tile links to product page on your shop -->
        <a href="<%= adData.shopProduct.url %>"
           target="_blank" rel="noopener nofollow sponsored"
           onclick="new Image().src='<%= adData.matchedOffer.adclick %>'; return true;">
            <img src="<%= adData.shopProduct.image %>" alt="<%= adData.shopProduct.name %>">
            <div class="product-name"><%= adData.shopProduct.name %></div>
            <div class="product-price">
                <%= new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(adData.shopProduct.price) %>
            </div>
        </a>

        <% } else { %>
        <!-- SPONSORED OFFERS: product from your catalog + CTA linking to merchant's shop -->
        <img src="<%= adData.shopProduct.image %>" alt="<%= adData.shopProduct.name %>">
        <div class="product-name"><%= adData.shopProduct.name %></div>
        <div class="product-price">
            <%= new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(adData.shopProduct.price) %>
        </div>
        <a class="cta-button"
           href="<%= adData.matchedOffer.offer_url %>"
           target="_blank" rel="noopener nofollow sponsored"
           onclick="new Image().src='<%= adData.matchedOffer.adclick %>'; return true;">
            View offer
        </a>
        <% } %>

    </div>
    <% }) %>

</div>

Step 5: Register Bid Response

After rendering, register each slot's filtered response with the SDK for viewability and impression tracking. Call registerBidResponse() once per rendered slot.

// Injected by the server into the page
const resolvedSlotsData = <%- JSON.stringify(resolvedSlots.map(s => ({
    filteredResponse: s.filteredResponse,
    containerId: s.containerId,
    matchedBidImpid: s.matchedBidImpid,
}))) %>;
// Runs in the browser after SDK loads
dlApi.cmd.push(function (dlApi) {
    resolvedSlotsData.forEach(function (slotData) {
        // Pass filteredResponse — only the seatbid for this specific slot.
        // Do NOT pass the full rawResponse with all seatbids.
        dlApi.registerBidResponse(slotData.filteredResponse, slotData.containerId);
    });
});
⚠️

registerBidResponse() counts impressions. Only call it when the ad is rendered and visible to the user — never for empty slots. For per-slot filtering rules and multi-slot patterns, see Register Bid Response.


Frontend SDK Setup

The SDK must be loaded on the page to enable viewability tracking via registerBidResponse(). Configure it with the same network ID used in the bidder request.

ParameterDescription
targetSite and area identifier (see details in Sponsored Single Tile — Step 1)
tidYour Ring DAS tenant ID (format: EA-XXXXXXX)
<script>
    dlApi = {
        target: "DEMO_PAGE/OOP",      // Format: SITE/AREA — use the area matching your page context
        tid: 'EA-<<NETWORK_ID>>',     // your tenant ID — provided by Ring DAS
        cmd: []
    };
</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>

Pass consent as soon as possible after the SDK loads:

dlApi.cmd.push(function (dlApi) {
    dlApi.consent({ npa: 0 });  // 0 = consent given, 1 = no consent
});

Set targeting context. For the slider, only main_category_id applies — page_type and search_query are search-listing-specific and do not apply here:

dlApi.cmd.push(function (dlApi) {
    dlApi.addKeyValue('main_category_id', 101);
    // page_type and search_query are not applicable for the slider format
});

What the Ad Server Returns

FieldDescription
adm.fields.feed.redirectToOffersDetermines the flow: false = Sponsored Product, true = Sponsored Offers
adm.fields.feed.offersArray of offer objects for this bid
adm.fields.feed.offers[].product_idProduct ID — use to fetch product details from your shop catalog
adm.fields.feed.offers[].offer_idOffer ID — used with product_id to fetch merchant-specific data in the Sponsored Offers flow
adm.fields.feed.offers[].offer_urlProdutc/Offer redirect URL — destination for the CTA button in the Sponsored Offers flow
adm.fields.feed.offers[].adclickClick tracking URL — fire as a pixel on user click (both flows)
bid.ext.dsa.behalfAdvertiser name (DSA compliance, requires regs.ext.dsa: 1)
bid.ext.dsa.paidEntity that paid for the ad (DSA compliance)
bid.impidMatches the imp[].id from the request — used to map each bid back to its slot group

Response structure is identical to Single Tile. Each bid in seatbid[].bid[] has the same adm structure as the Sponsored Single Tile format. The key difference is that you resolve multiple bids (one per slider slot) instead of a single bid.


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 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 catalog/database

Testing Your Integration

For testing backend integrations without running live campaigns, see Ad Preview & Debug Mode. It covers adbeta creative preview, test_area / test_site / test_kwrd overrides, and end-to-end tracking with X-ADP-EVENT-TRACK-ID.


Related