Brand Store (Mobile Apps)
Native iOS/Android integration for Brand Store format without JS SDK
Brand Store (Mobile Apps)
No SDK Required: This guide shows direct HTTP API integration for native mobile apps. For web integration with JavaScript SDK, see Brand Store (Web).
A button on the OOP (product detail page) displayed below the product that redirects users directly to the brand's official store page.
What Is Brand Store?
Brand Store is a sponsored button that appears on product detail pages in your mobile app. When a user taps it, they are redirected to the brand's (manufacturer's) official store where they can purchase the product directly.
Key concept: The ad server tells you IF a campaign is active for a given brand. Your app decides WHETHER to display the button based on your own business logic.
How It Works
What the Ad Server Provides
Based on the manufacturer_id you pass, the ad server checks if there is an active campaign for this brand (manufacturer). If yes, it returns:
- Confirmation that a sponsored ad exists
- Click tracking URL for revenue attribution
- Impression and viewability tracking endpoints
Important: The ad server only provides tracking information. All product display data (logo, price, URL) comes from YOUR app's data.
What Your App Must Check
Before displaying the button, your app should verify:
- Brand has a store page - Does this manufacturer have a brand shop in your system?
- Product offer exists - Does the brand's store have an offer for the current product?
- Price is competitive - Is the brand's offer cheaper than (or equal to) the current best price?
Display logic: Only show the Brand Store button if all conditions are met: active campaign (from ad server) + brand shop exists + competitive offer (from your app's data).
Data Sources
| Source | Data |
|---|---|
| Ad Server | Whether campaign is active, click tracking URL, impression/viewability tracking |
| Your App | Brand shop URL, logo, price, shop name, offer availability |
Data Flow
- Pass
manufacturer_idto ad server via bidder request - Ad server returns tracking info IF campaign is active for this brand
- Your app checks: Does brand have a store? Is offer available and competitive?
- If all conditions met → render button with your app data + ad server tracking
Live Demo
See the working implementation:
- Live Preview - See how the format looks
- Mobile Integration Playground - Interactive step-by-step guide
Integration Flow Overview
sequenceDiagram
participant App as Mobile App
participant ShopAPI as Shop API/Database
participant Bidder as Ring DAS
participant Events as Events Tracking API
App->>Bidder: 1. Fetch Ad (GET /bid)
Bidder-->>App: Response with gctx, lctx, tracking URLs
Note over App: 2. Parse response
App->>ShopAPI: 3. Fetch brand store data
ShopAPI-->>App: Brand shop URL, logo, price, availability
Note over App: 4. Verify conditions:<br/>- Brand shop exists?<br/>- Offer available?<br/>- Price competitive?
alt All conditions met
Note over App: 5. Render button with shop data
App->>Events: 6. Fire Impression (/ems)
Note over App: Wait for viewability (50% visible, 1s)
App->>Events: 7. Fire Active View (/av)
Note over App: User taps ad
App->>Events: 8. Fire Click Tracking
Note over App: Open shop URL
else Conditions not met
Note over App: Hide ad slot
end
Step 1: Fetch Ad from Bidder
Make a GET request to the Ring DAS bidder endpoint.
Request Configuration
| Parameter | Description |
|---|---|
BIDDER_URL | Bidder endpoint URL - provided by your Ring DAS account manager |
NETWORK_ID | Your Ring DAS network ID (without EA- prefix) |
tmax | Request timeout in milliseconds |
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: listing, search, product_card, main, other |
imp[].tagid | Fixed value brand_store for this format |
Targeting Parameters
| Parameter | Description |
|---|---|
ext.keyvalues.manufacturer_id | Manufacturer identifier for targeting |
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.ext.dsa | Set to 1 to request DSA transparency info in response |
User Identifier
| Parameter | Description |
|---|---|
user.ext.ids.lu | User identifier (see User Identifier Format below) |
Examples:
// iOS - Swift
import Foundation
let networkId = "7012768" // your Ring DAS network ID
let bidderUrl = "https://das.idealo.com/\(networkId)/bid"
let requestBody: [String: Any] = [
"id": UUID().uuidString,
"imp": [[
"id": "imp-1",
"tagid": "brand_store",
"secure": 1,
"native": ["request": "{}"]
]],
"site": [
"id": "demo_page", // your site ID
"ext": ["area": "product_card"]
],
"user": [
"ext": [
"npa": false, // false = consent given
"ids": ["lu": generateUserId()]
]
],
"ext": [
"network": networkId,
"keyvalues": ["manufacturer_id": "12345678"],
"is_non_prebid_request": true
],
"regs": [
"gdpr": 1,
"ext": ["dsa": 1]
],
"tmax": 1000
]
// Encode and send as GET request
let jsonData = try! JSONSerialization.data(withJSONObject: requestBody)
let jsonString = String(data: jsonData, encoding: .utf8)!
let encoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: "\(bidderUrl)?data=\(encoded)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else { return }
// Handle 204 No Content (no ad available)
if httpResponse.statusCode == 204 {
print("No ad available")
return
}
// Parse response...
}.resume()// Android - Kotlin
import okhttp3.*
import org.json.JSONArray
import org.json.JSONObject
import java.net.URLEncoder
val networkId = "7012768" // your Ring DAS network ID
val bidderUrl = "https://das.idealo.com/$networkId/bid"
val requestBody = JSONObject().apply {
put("id", java.util.UUID.randomUUID().toString())
put("imp", JSONArray().put(JSONObject().apply {
put("id", "imp-1")
put("tagid", "brand_store")
put("secure", 1)
put("native", JSONObject().put("request", "{}"))
}))
put("site", JSONObject().apply {
put("id", "demo_page") // your site ID
put("ext", JSONObject().put("area", "product_card"))
})
put("user", JSONObject().apply {
put("ext", JSONObject().apply {
put("npa", false) // false = consent given
put("ids", JSONObject().put("lu", generateUserId()))
})
})
put("ext", JSONObject().apply {
put("network", networkId)
put("keyvalues", JSONObject().put("manufacturer_id", "12345678"))
put("is_non_prebid_request", true)
})
put("regs", JSONObject().apply {
put("gdpr", 1)
put("ext", JSONObject().put("dsa", 1))
})
put("tmax", 1000)
}
// Encode and send as GET request
val encoded = URLEncoder.encode(requestBody.toString(), "UTF-8")
val request = Request.Builder().url("$bidderUrl?data=$encoded").build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
// Handle 204 No Content (no ad available)
if (response.code == 204) {
println("No ad available")
return
}
// Parse response...
}
override fun onFailure(call: Call, e: IOException) { }
})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
Extract tracking data from the bid response.
| Field | Description |
|---|---|
gctx | Global context - required for impression/viewability tracking |
lctx | Local context - required for impression/viewability tracking |
meta.adclick | Click tracking base URL |
fields.click | Click parameter to append to tracking URL |
bid.ext.dsa | DSA transparency info (if requested) |
Examples:
// iOS - Swift
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let seatbid = json["seatbid"] as? [[String: Any]],
let bid = seatbid.first?["bid"] as? [[String: Any]],
let firstBid = bid.first,
let admString = firstBid["adm"] as? String,
let admData = admString.data(using: .utf8),
let adm = try? JSONSerialization.jsonObject(with: admData) as? [String: Any] else {
return
}
// Extract tracking data
let gctx = adm["gctx"] as? String
let lctx = adm["lctx"] as? String
// Build click tracking URL
if let meta = adm["meta"] as? [String: Any],
let adclick = meta["adclick"] as? String,
let fields = adm["fields"] as? [String: Any],
let click = fields["click"] as? String {
let clickUrl = adclick + (click.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
}
// DSA info (optional)
let dsaInfo = (firstBid["ext"] as? [String: Any])?["dsa"] as? [String: Any]// Android - Kotlin
response.body?.string()?.let { body ->
val json = JSONObject(body)
val seatbid = json.optJSONArray("seatbid") ?: return
val bid = seatbid.optJSONObject(0)?.optJSONArray("bid")?.optJSONObject(0) ?: return
val adm = JSONObject(bid.getString("adm"))
// Extract tracking data
val gctx = adm.optString("gctx")
val lctx = adm.optString("lctx")
// Build click tracking URL
val meta = adm.optJSONObject("meta")
val fields = adm.optJSONObject("fields")
val adclick = meta?.optString("adclick", "") ?: ""
val click = fields?.optString("click", "") ?: ""
val clickUrl = adclick + URLEncoder.encode(click, "UTF-8")
// DSA info (optional)
val dsaInfo = bid.optJSONObject("ext")?.optJSONObject("dsa")
}Step 3: Render with Shop Data
Use YOUR app's product data to render the ad. The ad server does NOT return display data.
// iOS - Swift
// YOUR shop data - NOT from ad server!
struct ShopData {
let productUrl = "https://apple.com/store/iphone"
let shopLogo = "https://example.com/apple-logo.svg"
let shopName = "Apple"
let price = "€899.00"
}
// Render button in your app's UI
// Attach click handler that:
// 1. Fires click tracking pixel
// 2. Opens shop URL// Android - Kotlin
// YOUR shop data - NOT from ad server!
data class ShopData(
val productUrl: String = "https://apple.com/store/iphone",
val shopLogo: String = "https://example.com/apple-logo.svg",
val shopName: String = "Apple",
val price: String = "€899.00"
)
// Render button in your app's UI
// Attach click handler that:
// 1. Fires click tracking pixel
// 2. Opens shop URLStep 4: Fire Impression Event
Fire this event when the ad is loaded and ready to display.
Endpoint: GET https://das.idealo.com/{NETWORK_ID}/v1/events-processor/ems
| Parameter | Description |
|---|---|
eventData | URL-encoded JSON with gctx and ems array |
gctx | Global context from bid response |
lctx | Local context from bid response |
is_measurable | Set to true to indicate viewability can be measured |
Examples:
// iOS - Swift
func fireImpression(gctx: String, lctx: String, networkId: String) {
let eventData: [String: Any] = [
"gctx": gctx,
"ems": [[
"lctx": lctx,
"is_measurable": true
]]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: eventData),
let jsonString = String(data: jsonData, encoding: .utf8),
let encoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "https://das.idealo.com/\(networkId)/v1/events-processor/ems?eventData=\(encoded)") else {
return
}
URLSession.shared.dataTask(with: url).resume()
}// Android - Kotlin
fun fireImpression(gctx: String, lctx: String, networkId: String) {
val eventData = JSONObject().apply {
put("gctx", gctx)
put("ems", JSONArray().put(JSONObject().apply {
put("lctx", lctx)
put("is_measurable", true)
}))
}
val encoded = URLEncoder.encode(eventData.toString(), "UTF-8")
val url = "https://das.idealo.com/$networkId/v1/events-processor/ems?eventData=$encoded"
client.newCall(Request.Builder().url(url).build()).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) {}
})
}Step 5: Fire Active View Event
Your App Must Implement Viewability Measurement!
Fire
/avonly when these conditions are met (MRC/IAB Standard):
- At least 50% of ad pixels are visible on screen
- Ad has been visible for at least 1 continuous second
Use platform APIs:
- iOS:
UIViewvisibility checks +CADisplayLinkorTimerfor duration- Android:
View.getGlobalVisibleRect()+Handlerfor duration tracking
Endpoint: GET https://das.idealo.com/{NETWORK_ID}/v1/events-processor/av
Examples:
// iOS - Swift
func fireActiveView(gctx: String, lctx: String, networkId: String) {
let eventData: [String: Any] = [
"gctx": gctx,
"ems": [[
"lctx": lctx
]]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: eventData),
let jsonString = String(data: jsonData, encoding: .utf8),
let encoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "https://das.idealo.com/\(networkId)/v1/events-processor/av?eventData=\(encoded)") else {
return
}
URLSession.shared.dataTask(with: url).resume()
}
// Example viewability implementation
class ViewabilityTracker {
private var timer: Timer?
private var visibleStartTime: Date?
private let requiredDuration: TimeInterval = 1.0
private let requiredVisibility: CGFloat = 0.5
func startTracking(adView: UIView, onViewable: @escaping () -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
guard let self = self else { return }
if self.isViewVisible(adView, threshold: self.requiredVisibility) {
if self.visibleStartTime == nil {
self.visibleStartTime = Date()
} else if Date().timeIntervalSince(self.visibleStartTime!) >= self.requiredDuration {
self.timer?.invalidate()
onViewable()
}
} else {
self.visibleStartTime = nil // Reset if visibility lost
}
}
}
private func isViewVisible(_ view: UIView, threshold: CGFloat) -> Bool {
guard let window = view.window else { return false }
let viewFrame = view.convert(view.bounds, to: window)
let intersection = viewFrame.intersection(window.bounds)
let visibleArea = intersection.width * intersection.height
let totalArea = viewFrame.width * viewFrame.height
return visibleArea / totalArea >= threshold
}
}// Android - Kotlin
fun fireActiveView(gctx: String, lctx: String, networkId: String) {
val eventData = JSONObject().apply {
put("gctx", gctx)
put("ems", JSONArray().put(JSONObject().put("lctx", lctx)))
}
val encoded = URLEncoder.encode(eventData.toString(), "UTF-8")
val url = "https://das.idealo.com/$networkId/v1/events-processor/av?eventData=$encoded"
client.newCall(Request.Builder().url(url).build()).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) {}
})
}
// Example viewability implementation
class ViewabilityTracker(
private val adView: View,
private val onViewable: () -> Unit
) {
private val handler = Handler(Looper.getMainLooper())
private var visibleStartTime: Long? = null
private val requiredDurationMs = 1000L
private val requiredVisibility = 0.5f
private val checkRunnable = object : Runnable {
override fun run() {
if (isViewVisible(adView, requiredVisibility)) {
if (visibleStartTime == null) {
visibleStartTime = System.currentTimeMillis()
} else if (System.currentTimeMillis() - visibleStartTime!! >= requiredDurationMs) {
onViewable()
return // Stop checking
}
} else {
visibleStartTime = null // Reset if visibility lost
}
handler.postDelayed(this, 100)
}
}
fun startTracking() {
handler.post(checkRunnable)
}
fun stopTracking() {
handler.removeCallbacks(checkRunnable)
}
private fun isViewVisible(view: View, threshold: Float): Boolean {
val visibleRect = Rect()
if (!view.getGlobalVisibleRect(visibleRect)) return false
val visibleArea = visibleRect.width() * visibleRect.height()
val totalArea = view.width * view.height
return visibleArea.toFloat() / totalArea >= threshold
}
}Step 6: Fire Click Tracking
Fire the click tracking pixel when the user taps the ad, then open the shop URL.
Examples:
// iOS - Swift
func handleAdClick(clickUrl: String, shopUrl: String) {
// 1. Fire click tracking pixel (non-blocking)
if let url = URL(string: clickUrl) {
URLSession.shared.dataTask(with: url).resume()
}
// 2. Open shop URL
if let url = URL(string: shopUrl) {
UIApplication.shared.open(url)
}
}// Android - Kotlin
fun handleAdClick(clickUrl: String, shopUrl: String, context: Context) {
// 1. Fire click tracking pixel (non-blocking)
client.newCall(Request.Builder().url(clickUrl).build()).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) {}
})
// 2. Open shop URL
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(shopUrl))
context.startActivity(intent)
}User Identifier
The lu identifier enables frequency capping, personalization, and other targeting features.
| Parameter | Description |
|---|---|
user.ext.ids.lu | Value from ea_uuid cookie on your shop's domain (set by Ring DAS) |
Example format: 202502051230001234567890 (timestamp + random digits)
Flexible format: The example above uses a timestamp-based format, but you can use any unique identifier (todo: nie jako LU, a nny klucz dogadany z kims z RING DAS, bo lu moze miec tylko taki format.) that your app already maintains (e.g., internal user ID, device ID, or custom session identifier). The key requirement is consistency - use the same identifier for the same user.
Critical: Persist the User Identifier!
Store this identifier persistently and reuse the same value across:
- All ad requests within a session
- All sessions for the same user
Why this matters:
- Frequency capping - Limits how often a user sees the same ad
- Budget pacing - Distributes advertiser spend evenly across users
- Attribution - Tracks conversions back to ad impressions
- Personalization - Enables relevant ad targeting
Generating a new
luon every app launch breaks these features and reduces ad effectiveness.
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 (images, fonts from your app)
- Product data from your own database
Complete Integration Example
Here's a complete Swift class showing the full integration flow:
// iOS - Swift - Complete Example
import Foundation
import UIKit
class BrandStoreAdManager {
private let networkId: String
private let bidderUrl: String
private let client = URLSession.shared
// Parsed ad data
private var gctx: String?
private var lctx: String?
private var clickUrl: String?
private var userId: String
init(networkId: String) {
self.networkId = networkId
self.bidderUrl = "https://das.idealo.com/\(networkId)/bid"
self.userId = Self.generateUserId()
}
// MARK: - Public API
func fetchAd(siteId: String, manufacturerId: String, completion: @escaping (Bool) -> Void) {
let requestBody = buildRequest(siteId: siteId, manufacturerId: manufacturerId)
guard let url = buildRequestURL(requestBody) else {
completion(false)
return
}
client.dataTask(with: url) { [weak self] data, response, _ in
guard let httpResponse = response as? HTTPURLResponse else {
completion(false)
return
}
if httpResponse.statusCode == 204 {
completion(false)
return
}
guard let data = data else {
completion(false)
return
}
self?.parseResponse(data)
completion(self?.gctx != nil)
}.resume()
}
func fireImpression() {
guard let gctx = gctx, let lctx = lctx else { return }
fireEvent(endpoint: "ems", gctx: gctx, lctx: lctx, isMeasurable: true)
}
func fireActiveView() {
guard let gctx = gctx, let lctx = lctx else { return }
fireEvent(endpoint: "av", gctx: gctx, lctx: lctx, isMeasurable: false)
}
func handleClick(shopUrl: String) {
// Fire click tracking
if let clickUrl = clickUrl, let url = URL(string: clickUrl) {
client.dataTask(with: url).resume()
}
// Open shop URL
if let url = URL(string: shopUrl) {
DispatchQueue.main.async {
UIApplication.shared.open(url)
}
}
}
// MARK: - Private Helpers
private func buildRequest(siteId: String, manufacturerId: String) -> [String: Any] {
return [
"id": UUID().uuidString,
"imp": [["id": "imp-1", "tagid": "brand_store", "secure": 1, "native": ["request": "{}"]]],
"site": ["id": siteId, "ext": ["area": "product_card"]],
"user": ["ext": ["npa": false, "ids": ["lu": userId]]],
"ext": ["network": networkId, "keyvalues": ["manufacturer_id": manufacturerId], "is_non_prebid_request": true],
"regs": ["gdpr": 1, "ext": ["dsa": 1]],
"tmax": 1000
]
}
private func buildRequestURL(_ body: [String: Any]) -> URL? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: body),
let jsonString = String(data: jsonData, encoding: .utf8),
let encoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}
return URL(string: "\(bidderUrl)?data=\(encoded)")
}
private func parseResponse(_ data: Data) {
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let seatbid = json["seatbid"] as? [[String: Any]],
let bid = seatbid.first?["bid"] as? [[String: Any]],
let firstBid = bid.first,
let admString = firstBid["adm"] as? String,
let admData = admString.data(using: .utf8),
let adm = try? JSONSerialization.jsonObject(with: admData) as? [String: Any] else {
return
}
gctx = adm["gctx"] as? String
lctx = adm["lctx"] as? String
if let meta = adm["meta"] as? [String: Any],
let adclick = meta["adclick"] as? String,
let fields = adm["fields"] as? [String: Any],
let click = fields["click"] as? String {
clickUrl = adclick + (click.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
}
}
private func fireEvent(endpoint: String, gctx: String, lctx: String, isMeasurable: Bool) {
var emsItem: [String: Any] = ["lctx": lctx]
if isMeasurable {
emsItem["is_measurable"] = true
}
let eventData: [String: Any] = ["gctx": gctx, "ems": [emsItem]]
guard let jsonData = try? JSONSerialization.data(withJSONObject: eventData),
let jsonString = String(data: jsonData, encoding: .utf8),
let encoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "https://das.idealo.com/\(networkId)/v1/events-processor/\(endpoint)?eventData=\(encoded)") else {
return
}
client.dataTask(with: url).resume()
}
private static func generateUserId() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmss"
let timestamp = formatter.string(from: Date())
let random = String(format: "%010d", Int.random(in: 0..<10_000_000_000))
return timestamp + random
}
}Related
- Brand Store (Web) - JavaScript SDK integration for web
- Format Overview - Compare available ad formats
- Ad Delivery Overview - Learn about Ring DAS ad delivery
Updated about 13 hours ago
