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
redirectToOffersflag that determines how the tile behaves:
redirectToOffers === false→ Sponsored Product: Tile shows a product. Click leads to the product page on the shop.redirectToOffers === true→ Sponsored 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.
| Source | Sponsored Product | Sponsored Offers |
|---|---|---|
| Ad Server | Product ID, click tracking, DSA info | Product ID, offer ID, redirect destination (offer_url), click tracking (adclick), 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: 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 via offer.offer_url. Click tracking fires via offer.adclick in both flows.
Data flow:
Sponsored Product (redirectToOffers === false):
- Your page requests an ad from Ring DAS
- Ad server returns
offers[]withproduct_idandredirectToOffers: false - Your shop fetches product details (image, price, name, URL) from your catalog using
product_id - Your shop renders the tile linking to the product page
- On click, a tracking pixel fires via the click tracking URL
Sponsored Offers (redirectToOffers === true):
- Your page requests an ad from Ring DAS
- Ad server returns
offers[]withproduct_id,offer_id, andredirectToOffers: true - Your shop fetches product details (image, price, name, merchant shop name) from your catalog using
product_id+offer_id - Your shop renders the tile with a CTA button showing the merchant's shop name
- On CTA click, a tracking pixel fires via
offer.adclickand 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.
| Parameter | Description |
|---|---|
target | Site and area identifier (see details below) |
tid | Your Ring DAS tenant ID (format: EA-XXXXXXX) |
dsainfo | Set 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
});| Parameter | Value | Description |
|---|---|---|
npa | 0 | Consent given - personalized ads enabled |
npa | 1 | No 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-Value | Description |
|---|---|
offer_ids | Product IDs currently displayed on the page |
category_ids | Category IDs for the current page context |
main_category_id | Main product category ID - primary category used for ad matching (product may belong to multiple categories, but has one main) |
IP | Page View ID — constant identifier for the current page load |
IV | Page View Unique ID — changes on every SPA view transition; equals IP on traditional pages |
TABID | Browser Tab Identifier — stable ID per browser tab session |
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
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
});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.
| Parameter | Description |
|---|---|
slot | Fixed value sponsored for this format |
div | Container element ID - required for viewability measurement |
tplCode | Fixed value 1746213/Sponsored-Product for Sponsored Single Tile |
asyncRender | Set to true to manually control when impression is counted |
opts.pos | Slot position (1, 2, ...) when using multiple slots on the same page |
opts.offers_limit | Optional. 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_limitis 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:
- Check
ad.fields.feed.redirectToOffersfrom the response - If
false→ Sponsored Product: tile links to the product page on your shop - If
true→ Sponsored 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.adclick || '') + '\';">'
+ '<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>';
}
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.adclick || '') + '\';">'
+ 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 fireoffer.adclickas 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 viaoffer.offer_url.
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
| Parameter | Description |
|---|---|
BIDDER_URL | Bidder endpoint URL - provided by your Ring DAS account manager |
NETWORK_ID | Your Ring DAS network ID (e.g., 1746213) |
tmax | Request timeout in milliseconds |
ext.src | Optional. Set to s2s for server-to-server requests |
ext.is_non_prebid_request | Set to true to receive the response in the correct server-to-server format (not Prebid.js format) |
Placement Parameters
These parameters define WHERE the ad appears and WHICH format to serve.
| Parameter | Description |
|---|---|
site.id | Your site identifier in Ring DAS |
site.ext.area | Page type context, UPPERCASE: HOMEPAGE, PRODUCT_DETAIL, LISTING, SEARCH |
imp[].tagid | product-tile for first/best position; product-tile2 for additional positions (requires imp[].ext.pos) |
imp[].ext.pos | Position index — required only for product-tile2 (e.g., 1 for second slot, 2 for third) |
Targeting Parameters
| Parameter | Description |
|---|---|
ext.offerids | List of product offer IDs from current page for contextual targeting and relevancy (optional) |
ext.categoryids | Category IDs for contextual targeting — applies globally to all impressions in the request (optional) |
ext.keyvalues.page_type | Page context type: SEARCH for search results pages, CATEGORY for category/listing pages (optional but recommended) |
ext.keyvalues.search_query | The user's search query string — applicable when page_type is SEARCH (optional) |
ext.excluded_offer_ids | List of offer IDs already shown organically on the page — prevents the same offer appearing as both organic and sponsored (optional) |
ext.keyvalues.IP | Page View ID — constant for the lifetime of a page load. See API Parameters Reference |
ext.keyvalues.IV | Page View Unique ID — changes on every SPA view transition; equals IP on traditional pages. See API Parameters Reference |
ext.keyvalues.TABID | Browser Tab Identifier — stable ID per browser tab session. See API Parameters Reference |
ext.keyvalues.ab_variant | A/B test variant identifier — string identifying which experiment variant the user is in (optional) |
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 if adclick URLs should only fire tracking pixel without redirect (shop handles redirect via href) |
imp[].ext.offers_limit | Optional. Maximum number of offers returned by the ad server. Request more than one to increase the chance of matching a product in your catalog |
Multiply impressions for higher fill rate: To increase the number of ad candidates returned by the bidder, 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 iterates all seatbids whoseimpidmatches any of the slot's impression IDs and returns the first catalog match.imp: [ { id: 'imp-1-0', tagid: 'product-tile', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } }, { id: 'imp-1-1', tagid: 'product-tile', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } }, { id: 'imp-1-2', tagid: 'product-tile', secure: 1, native: { request: '{}' }, ext: { no_redirect: true, offers_limit: 3 } }, ]Use one multiply group per logical slot. For two distinct slots (
product-tileandproduct-tile2), define two separate groups of replicated impressions.
Consent handling: You can pass consent via
user.ext.npa(simple flag) OR viaregs.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.
| Parameter | Description |
|---|---|
user.eids | OpenRTB 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.lu | Local 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 asatype: 500(no consent required) and your device/tracking ID asatype: 1(consent required).user.ext.ids.lu— The value from theea_uuidcookie 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.eidsfield 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: [{
id: 'imp-1',
tagid: 'product-tile', // product-tile (first/best) or product-tile2 (additional positions, requires pos)
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
}
}],
site: {
id: 'DEMO_PAGE', // your site ID (UPPERCASE)
ext: {
area: 'SEARCH' // SEARCH for search results pages, LISTING/CATEGORY for category pages
}
},
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: {
page_type: 'SEARCH', // SEARCH or CATEGORY — page context
search_query: 'nike shoes', // optional: search query for SEARCH pages
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: ['12345', '67890'], // optional: product IDs from current page
categoryids: [101, 205], // optional: category IDs for targeting
excluded_offer_ids: ['1011'], // optional: exclude organic offer IDs to prevent duplicates
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 Contentwith 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,
adm: adm,
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.adclick %>';">
<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>
<a class="cta-button" href="<%= adData.offer.offer_url %>"
target="_blank" rel="noopener nofollow sponsored"
onclick="new Image().src='<%= adData.offer.adclick %>';">
<%= adData.shopData.shopName || 'Shop' %>
</a>
</div>
<% } %>
</div>
<!-- Register for viewability and impression tracking -->
<script>
dlApi.cmd.push(function () {
dlApi.registerBidResponse(<%- JSON.stringify(adData.adm) %>, 'ad-sponsored');
});
</script>
registerBidResponse()counts impressions. Only call it when the ad is rendered — never for empty slots. For multi-slot patterns, see Register Bid Response.
What the Ad Server Returns
| Field | Description |
|---|---|
ad.fields.feed.redirectToOffers | Determines the flow: false = Sponsored Product, true = Sponsored Offers |
ad.fields.feed.offers | Array of offer objects |
ad.fields.feed.offers[].product_id | Product ID — use to fetch product details from your shop catalog |
ad.fields.feed.offers[].offer_id | Offer ID — used with product_id to fetch product details in the Sponsored Offers flow |
ad.fields.feed.offers[].adclick | Click tracking URL — fire as pixel on click |
ad.fields.feed.offers[].offer_url | Redirect URL — destination for the CTA button (Sponsored Offers flow) |
ad.fields.feed.offers[].offer_custom_fields | Optional. 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 |
ad.dsa.behalf | Advertiser name (DSA compliance, requires dsainfo: 1) |
ad.dsa.paid | Entity that paid for the ad (DSA compliance) |
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.
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: 'product-tile', // first/best position — no pos required
secure: 1,
native: { request: "{}" },
ext: {}
},
{
id: 'ad-sponsored-2', // unique ID for second slot
tagid: 'product-tile2', // additional positions — pos required
secure: 1,
native: { request: "{}" },
ext: { pos: 1 } // position index (1, 2, ...)
}
],
// ... 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
});
registerBidResponse()counts impressions. Only call it when the ad is rendered — never for empty slots. For multi-slot patterns, see Register Bid Response.
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
- Format Overview - Compare available ad formats
- Branded Products - Homepage banner with brand logo and curated products
- Brand Store (Web) - Button format for product detail pages
Updated 19 days ago
