Branded Products
A homepage display banner showing brand logo and curated products
Branded Products
A homepage display banner showing the brand logo and a curated set of products selected by the brand.
How It Works
Important: This is a hybrid format. The ad server provides the brand logo and a list of product IDs. Your shop must fetch product details (image, price, name, URL) for each product ID from your own system.
| Source | Data |
|---|---|
| Ad Server | Brand logo (bannerImage), logo click URL (urlRedirect, optional), list of product IDs (offers[].product_id), click tracking, DSA info |
| Your Shop | Product details for each ID: image, price, name, URL |
Data flow:
- Ad server returns
bannerImage(brand logo) andoffers[]array withproduct_idfor each product - Your shop extracts each
product_idfrom the offers - Your shop fetches product details (image, price, name, URL) from your product database
- Your shop renders the banner with combined data
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 homepage
Page->>DAS: 2. Fetch Ad (fetchNativeAd / bidder)
DAS-->>Page: Brand logo + product IDs
Note over Page: 3. Extract product IDs from response
Page->>ShopAPI: 4. Fetch product details for IDs
ShopAPI-->>Page: Product images, prices, names, URLs
Note over Page: 5. Combine:<br/>- Logo from DAS<br/>- Products from Shop<br/>- Tracking from DAS
Page->>User: 6. Render branded products banner
Note over Page: Fire impression tracking
User->>Page: 7. Click product
Page->>DAS: Fire click tracking
Page->>User: Redirect to product page
Number of products is configurable. The examples in this documentation show 5 products, but your shop decides how many to display (e.g., 2, 5, 7, or more). Simply adjust the limit in your rendering code.
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/HOMEPAGE)Examples:
- Desktop homepage:
DEMO_PAGE/HOMEPAGE- Mobile product page:
M_DEMO_PAGE/PRODUCT_DETAIL- Desktop category listing:
DEMO_PAGE/LISTING
<script>
dlApi = {
target: "DEMO_PAGE/HOMEPAGE", // 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 fetch the ad. The ad server returns the brand logo and product IDs - you must fetch product details from your shop.
| Parameter | Description |
|---|---|
slot | Fixed value display for this format |
div | Container element ID - required for viewability measurement |
tplCode | Fixed value 1746213/Sponsored-Product-Plus for Branded Products |
asyncRender | Set to true to manually control when impression is counted |
opts.offers_limit | Maximum number of products returned from ad server |
dlApi.cmd.push(function (dlApi) {
dlApi.fetchNativeAd({
slot: 'display', // fixed value for Branded Products
div: 'ad-branded-products', // container ID for viewability tracking
opts: {
offers_limit: 5 // maximum number of products returned from ad server
},
tplCode: '1746213/Sponsored-Product-Plus', // fixed value for Branded Products
asyncRender: true, // manually count impression with ad.render()
}).then(async function (ad) {
if (!ad) {
console.log('[Branded Products] No ad available');
return;
}
console.log('[Branded Products] Ad response:', ad);
// 1. Extract data from ad server
const bannerImage = ad.fields?.bannerImage || ''; // Brand logo URL
const urlRedirect = ad.fields?.urlRedirect || ''; // Logo click URL (optional)
const offers = ad.fields?.feed?.offers || [];
const dsaInfo = ad.dsa;
// 2. Extract product IDs from offers
const productIds = offers.map(offer => offer.product_id);
console.log('[Branded Products] Product IDs to fetch:', productIds);
// 3. Fetch product details from YOUR shop (implement this function)
const productDetails = await fetchProductDetailsFromShop(productIds);
// 4. Combine shop data with ad server tracking URLs
const productsWithTracking = productDetails.map((product, index) => ({
...product,
trackingUrl: offers[index]?.adclick || ''
}));
// 5. Render the ad with combined data
renderBrandedProducts(bannerImage, productsWithTracking, dsaInfo, urlRedirect);
// 6. Count impression after rendering
ad.render();
}).catch(function (err) {
console.error('[Branded Products] Ad could not be loaded:', err);
});
// Trigger ad fetch
dlApi.fetch();
});Step 5: Fetch Product Details from Your Shop
Implement a function to fetch product details based on product IDs. This connects to YOUR product database/API.
/**
* Fetch product details from your shop's product API
* @param {string[]} productIds - Array of product IDs from ad server
* @returns {Promise<Array>} - Product details with image, price, name, URL
*/
async function fetchProductDetailsFromShop(productIds) {
// Example: Call your shop's product API
const products = await Promise.all(
productIds.map(async (productId) => {
// Replace with your actual product API endpoint
const response = await fetch(`/api/products/${productId}`);
const product = await response.json();
return {
product_id: productId,
image: product.imageUrl,
name: product.title,
price: product.price,
url: product.detailPageUrl
};
})
);
return products.filter(p => p !== null); // Filter out any failed fetches
}Implementation note: The onclick handler shown below is an example. Your shop is responsible for implementing click tracking - simply fire a tracking pixel on product click using the constructed tracking URL.
Example code: The rendering function below is provided as a documentation example. Adapt it to your shop's templating system and styling requirements.
Step 6: Render the Ad
Render the brand logo and product tiles with the combined data:
function renderBrandedProducts(bannerImage, products, dsaInfo, urlRedirect) {
const container = document.getElementById('ad-branded-products');
const MAX_PRODUCTS = 5;
// Build products HTML using shop data + ad server tracking URLs
let productsHtml = '';
products.slice(0, MAX_PRODUCTS).forEach(function(product) {
const price = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(product.price);
productsHtml += `
<a href="${product.url}" class="product-tile"
target="_blank" rel="noopener nofollow sponsored"
onclick="new Image().src='${product.trackingUrl}';">
<img src="${product.image}" alt="${escapeHtml(product.name)}">
<div class="product-name">${escapeHtml(product.name)}</div>
<div class="product-price">${price}</div>
</a>
`;
});
// Build DSA info HTML (Digital Services Act transparency)
let dsaHtml = '';
if (dsaInfo) {
dsaHtml = `
<div class="dsa-info">
<span class="dsa-icon" title="Ad transparency">i</span>
<div class="dsa-tooltip">
<div>Advertiser: ${dsaInfo.behalf || 'N/A'}</div>
<div>Paid by: ${dsaInfo.paid || 'N/A'}</div>
</div>
</div>
`;
}
// Render complete format
// bannerImage = from ad server
// products = from YOUR shop + trackingUrl from ad server
container.innerHTML = `
<div class="sponsored-header">
<span class="sponsored-badge">Sponsored</span>
${dsaHtml}
</div>
<div class="brand-section">
${urlRedirect
? `<a href="${escapeHtml(urlRedirect)}" target="_blank" rel="noopener nofollow sponsored">
<img class="brand-logo" src="${escapeHtml(bannerImage)}" alt="Brand Logo">
</a>`
: `<img class="brand-logo" src="${bannerImage}" alt="Brand Logo">`}
</div>
<div class="products-section">
${productsHtml}
</div>
`;
}
// Helper function to escape HTML special characters
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}Backend Integration (SSR)
For server-side rendering, fetch ad data from the bidder, then fetch product details from your shop 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 for Branded Products) |
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 | Fixed value display for this format |
Targeting Parameters
| Parameter | Description |
|---|---|
ext.offerids | List of product offer IDs from current page for contextual targeting and relevancy (optional) |
ext.categoryids | Product category IDs for contextual targeting — applies globally to all impressions in the request (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) |
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}`; // Branded Products 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: 'display', // fixed value for Branded Products
secure: 1,
native: { request: "{}" },
ext: {
offers_limit: 5, // maximum number of products returned from ad server
no_redirect: true // adclick URLs fire tracking only, no redirect
}
}],
site: {
id: 'DEMO_PAGE', // your site ID (UPPERCASE)
ext: {
area: 'HOMEPAGE' // page type (UPPERCASE): HOMEPAGE, PRODUCT_DETAIL, LISTING, SEARCH
}
},
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: {
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
}, // optional targeting parameters
offerids: ['2345', '67890'], // optional: product IDs from current page
categoryids: [101, 205], // optional: category IDs for targeting
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');
}
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 Fetch Product Details
Extract the brand logo and product IDs, then fetch product details from your shop:
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 data from ad server
const bannerImage = adm.fields?.bannerImage || null; // Brand logo URL
const urlRedirect = adm.fields?.urlRedirect || null; // Logo click URL (optional)
const offers = adm.fields?.feed?.offers || []; // Array with product_id
// 2. Extract product IDs
const productIds = offers.map(offer => offer.product_id);
console.log('Product IDs from ad server:', productIds);
// 3. Fetch product details from YOUR shop database
const shopProducts = await fetchProductsFromDatabase(productIds);
// 4. Combine shop data with ad server tracking URLs
const products = shopProducts.map((product, index) => ({
...product,
trackingUrl: offers[index]?.adclick || ''
}));
// 5. Extract DSA transparency info
const dsaInfo = bid.ext?.dsa ? {
advertiser: bid.ext.dsa.behalf,
payer: bid.ext.dsa.paid
} : null;
// 6. Prepare data for rendering
const adData = {
bannerImage, // From ad server
urlRedirect, // Logo click URL from ad server (optional)
products, // From YOUR shop + trackingUrl from ad server
dsaInfo,
adm: adm,
containerId: 'ad-branded-products'
};
/**
* Fetch product details from your database
* @param {string[]} productIds - Product IDs from ad server
*/
async function fetchProductsFromDatabase(productIds) {
// Replace with your actual database query
const products = await db.products.findMany({
where: { id: { in: productIds } },
select: {
id: true,
imageUrl: true,
title: true,
price: true,
detailPageUrl: true
}
});
return products.map(p => ({
product_id: p.id,
image: p.imageUrl,
name: p.title,
price: p.price,
url: p.detailPageUrl
}));
}Step 3: Render HTML + Register Response
Render the branded products banner combining ad server data (logo) with your shop data (products):
<!-- Render branded products banner -->
<div id="ad-branded-products" class="branded-products">
<div class="sponsored-header">
<span class="sponsored-badge">Sponsored</span>
<% if (adData.dsaInfo) { %>
<div class="dsa-info">
<span>Advertiser: <%= adData.dsaInfo.advertiser %></span>
<span>Paid by: <%= adData.dsaInfo.payer %></span>
</div>
<% } %>
</div>
<!-- Brand logo from AD SERVER -->
<div class="brand-section">
<% if (adData.urlRedirect) { %>
<a href="<%= adData.urlRedirect %>" target="_blank" rel="noopener nofollow sponsored">
<img class="brand-logo" src="<%= adData.bannerImage %>" alt="Brand Logo">
</a>
<% } else { %>
<img class="brand-logo" src="<%= adData.bannerImage %>" alt="Brand Logo">
<% } %>
</div>
<!-- Product details from YOUR SHOP + tracking URLs from AD SERVER -->
<div class="products-section">
<% adData.products.slice(0, 5).forEach(function(product) { %>
<a href="<%= product.url %>" class="product-tile"
target="_blank" rel="noopener nofollow sponsored"
onclick="new Image().src='<%= product.trackingUrl %>';">
<img src="<%= product.image %>" alt="<%= product.name %>">
<div class="product-name"><%= product.name %></div>
<div class="product-price">
<%= new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(product.price) %>
</div>
</a>
<% }); %>
</div>
</div>
<!-- Register for viewability and impression tracking -->
<script>
dlApi.cmd.push(function () {
dlApi.registerBidResponse(<%- JSON.stringify(adData.adm) %>, 'ad-branded-products');
});
</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.bannerImage | Brand logo URL (render directly) |
ad.fields.urlRedirect | Brand logo click destination URL (optional). When present, wrap the brand logo in a link to this URL. |
ad.fields.feed.offers | Array of product objects |
ad.fields.feed.offers[].product_id | Product ID - use to fetch details from your shop |
ad.fields.feed.offers[].adclick | Click tracking URL — fire as pixel on click |
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) |
ad.meta.adid | Ad identifier |
Key difference from Brand Store: The ad server provides the brand logo AND tells you which products to display (via
product_id). Your shop must fetch the actual product details (image, price, name, URL) from your own product 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.
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 ad slots on a single page, include multiple impressions in one request with unique IDs.
Backend (Bidder API):
const requestBody = {
id: 'request-123',
imp: [
{
id: 'imp-0', // unique ID for first slot
tagid: 'display',
secure: 1,
native: { request: "{}" },
ext: { pos: 1, offers_limit: 5 }
},
{
id: 'imp-1', // unique ID for second slot
tagid: 'display',
secure: 1,
native: { request: "{}" },
ext: { pos: 2, offers_limit: 5 }
}
],
// ... 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
// Render ad for this specific slot
});
registerBidResponse()counts impressions. Only call it when the ad is rendered — never for empty slots. For multi-slot patterns, see Register Bid Response.
Related
- Format Overview - Compare available ad formats
- Brand Store (Web) - Button format for product detail pages
- Ad Delivery Overview - Learn about Ring DAS ad delivery
Updated 4 days ago
Learn how to integrate the Sponsored Product / Offer (Single Tile) format
