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_offer_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_offer_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_offer_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)
ext.keyvalues.IPPage View ID — constant for the lifetime of a page load. See API Parameters Reference
ext.keyvalues.IVPage View Unique ID — changes on every SPA view transition; equals IP on traditional pages. See API Parameters Reference
ext.keyvalues.TABIDBrowser Tab Identifier — stable ID per browser tab session. See API Parameters Reference
ext.keyvalues.ab_variantA/B test variant identifier — string identifying which experiment variant the user is in (optional)
ext.srcOptional. Set to s2s for server-to-server requests
ext.is_non_prebid_requestSet to true to receive the response in the correct server-to-server format (not Prebid.js format)

excluded_offer_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

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.eidsOpenRTB Extended Identity Array — pass user identifiers with per-ID GDPR consent metadata. Supports session-level (atype: 500, no consent required) and device-level (atype: 1, consent required) identifiers. See API Parameters Reference for full field reference.
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).

User identifiers:

  • user.eids — Pass your own session and/or tracking identifiers with GDPR consent metadata. Provide your session ID as atype: 500 (no consent required) and your device/tracking ID as atype: 1 (consent required).
  • user.ext.ids.lu — The value from the ea_uuid cookie set by Ring DAS. Used as Ring DAS's internal session identifier.

Both can be sent simultaneously. See API Parameters Reference for the full user.eids field reference.

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

// User identifiers — replace with actual values from your system
const SESSION_ID = 'your-session-id';      // session-level identifier (no GDPR consent required)
const TRACKING_ID = 'your-tracking-id';    // device-level tracking identifier (GDPR consent required)

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: {
        eids: [{
            source: 'your-domain.com',
            inserter: 'your-domain.com',
            uids: [
                {
                    id: SESSION_ID,
                    atype: 500,
                    ext: { id_type: 'session', consent_required: false }
                },
                {
                    id: TRACKING_ID,
                    atype: 1,
                    ext: { id_type: 'tracking', consent_required: true }
                }
            ]
        }],
        ext: {
            npa: false,                               // false = consent given, true = no consent
            ids: {
                lu: '202408221036415499301131'        // Ring DAS internal session ID (from ea_uuid cookie)
            }
        }
    },
    ext: {
        network: NETWORK_ID,
        keyvalues: {
            main_category_id: 101,                    // main category ID of the viewed product
            IP: '202603041502158687149647',            // page view ID
            IV: '202603041502158687149647',            // page view unique ID
            TABID: 'tab-7f3e2a',                      // browser tab identifier
            ab_variant: 'variant-b',                  // optional: A/B test variant identifier
        },
        offerids: ['10', '20', '1234'],               // optional: product IDs from current page
        excluded_offer_ids: ['10', '102'],           // organic offer IDs from price table — prevents duplicate display
        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,
                        redirectToOffers,
                        shopProduct: product,
                        dsaInfo,
                        containerId: slot.containerId,
                        adm: bid.adm,   // raw adm string — passed to registerBidResponse()
                    };
                }
            }
        }
    }
    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: 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 4: Register Bid Response

After rendering, register each slot's adm 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 => ({
    adm: s.adm,
    containerId: s.containerId,
}))) %>;
// Runs in the browser after SDK loads
dlApi.cmd.push(function (dlApi) {
    resolvedSlotsData.forEach(function (slotData) {
        dlApi.registerBidResponse(slotData.adm, slotData.containerId);
    });
});
⚠️

registerBidResponse() counts impressions. Only call it when the ad is rendered — never for empty slots. For 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 before fetching the ad:

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)
IPPage View ID — constant identifier for the current page load
IVPage View Unique ID — changes on every SPA view transition; equals IP on traditional pages
TABIDBrowser Tab Identifier — stable ID per browser tab session
ℹ️

page_type and search_query are search-listing-specific and do not apply to the slider format.

dlApi.cmd.push(function (dlApi) {
    dlApi.addKeyValue('main_category_id', 101);
    dlApi.addKeyValue('IP', '202603041502158687149647');   // page view ID
    dlApi.addKeyValue('IV', '202603041502158687149647');   // page view unique ID (changes on SPA transitions)
    dlApi.addKeyValue('TABID', 'tab-7f3e2a');              // browser tab identifier
});

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)
adm.fields.feed.offers[].offer_custom_fieldsOptional. Object with custom fields for the offer. May contain manufacturer_id — the manufacturer identifier for the offered product. Both the object and individual fields may be absent
adm.fields.feed.offers[].olctxOffer local context — required for per-offer impression counting when offers are served directly from the bidder. Pass each rendered offer's olctx in the emission event
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

Preview & Test Your Ads

Ad Preview & Debug Mode serves two distinct audiences — developers integrating the format and business stakeholders managing live campaigns.

For developers: Test backend integrations and verify ad rendering without running live campaigns. Use query string overrides (test_site, test_area, test_kwrd) to simulate different targeting contexts and adbeta to force a specific creative from any line item.

For advertisers and campaign managers: Preview a specific creative on a real publisher page before campaign launch — or verify it after changes. No development tools or server access required. Share a single URL (with the right query parameters) and anyone opening that link sees the exact creative that will run in production.

See Ad Preview & Debug Mode for the full parameter reference, backend implementation guide, and end-to-end tracking with X-ADP-EVENT-TRACK-ID.


Related