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
redirectToOffersflag that determines how each tile behaves:
redirectToOffers === false→ Sponsored Product: Tile shows a product. Click leads to the product page on your shop.redirectToOffers === true→ Sponsored 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.
| Source | Sponsored Product | Sponsored Offers |
|---|---|---|
| Ad Server | Product ID, click tracking, DSA info | Product ID, offer ID, redirect destination, click tracking, DSA info |
| Your Shop | Product 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
| Parameter | Description |
|---|---|
site.id | Your site identifier in Ring DAS |
site.ext.area | Page type context where the slider appears, UPPERCASE (e.g., OOP for Offers of Product pages). Provided by your Ring DAS account manager |
imp[].tagid | product-tile-slider for first/best slider position; product-tile-slider2 for additional positions (requires imp[].ext.pos) |
imp[].ext.pos | Position index — required only for product-tile-slider2 (e.g., 1 for second slot, 2 for third) |
Targeting Parameters
| Parameter | Description |
|---|---|
ext.keyvalues.main_category_id | Main category ID of the product currently viewed — primary signal for ad matching |
ext.categoryids | Category IDs for contextual targeting — applies globally to all impressions in the request (optional) |
ext.excluded_offers_ids | List 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_idsprevents 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
imparray — each with a uniqueidbut the sametagid. 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
| Parameter | Description |
|---|---|
user.ext.npa | Consent flag: false = consent given, true = no consent |
regs.gdpr | GDPR applies flag: 1 = GDPR applies, 0 = does not apply |
regs.gpp | GPP consent string (optional) — TCF-compliant consent string from your CMP |
regs.ext.dsa | Set to 1 to request DSA transparency info in response |
Ad Behavior Parameters
| Parameter | Description |
|---|---|
imp[].ext.no_redirect | Set to true so adclick URLs fire tracking pixel only (shop handles redirect via href) |
imp[].ext.offers_limit | Maximum 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 %> · 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.
| Parameter | Description |
|---|---|
target | Site and area identifier (see details in Sponsored Single Tile — Step 1) |
tid | Your 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
| Field | Description |
|---|---|
adm.fields.feed.redirectToOffers | Determines the flow: false = Sponsored Product, true = Sponsored Offers |
adm.fields.feed.offers | Array of offer objects for this bid |
adm.fields.feed.offers[].product_id | Product ID — use to fetch product details from your shop catalog |
adm.fields.feed.offers[].offer_id | Offer ID — used with product_id to fetch merchant-specific data in the Sponsored Offers flow |
adm.fields.feed.offers[].offer_url | Produtc/Offer redirect URL — destination for the CTA button in the Sponsored Offers flow |
adm.fields.feed.offers[].adclick | Click tracking URL — fire as a pixel on user click (both flows) |
bid.ext.dsa.behalf | Advertiser name (DSA compliance, requires regs.ext.dsa: 1) |
bid.ext.dsa.paid | Entity that paid for the ad (DSA compliance) |
bid.impid | Matches 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 sameadmstructure 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
adbetacreative preview,test_area/test_site/test_kwrdoverrides, and end-to-end tracking withX-ADP-EVENT-TRACK-ID.
Related
- Format Overview - Compare available ad formats
- Sponsored Single Tile - Single sponsored tile for search and category pages
- Branded Products - Homepage banner with brand logo and curated products
Updated about 14 hours ago
