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:

  1. Brand has a store page - Does this manufacturer have a brand shop in your system?
  2. Product offer exists - Does the brand's store have an offer for the current product?
  3. 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

SourceData
Ad ServerWhether campaign is active, click tracking URL, impression/viewability tracking
Your AppBrand shop URL, logo, price, shop name, offer availability

Data Flow

  1. Pass manufacturer_id to ad server via bidder request
  2. Ad server returns tracking info IF campaign is active for this brand
  3. Your app checks: Does brand have a store? Is offer available and competitive?
  4. If all conditions met → render button with your app data + ad server tracking

Live Demo

See the working implementation:


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

ParameterDescription
BIDDER_URLBidder endpoint URL - provided by your Ring DAS account manager
NETWORK_IDYour Ring DAS network ID (without EA- prefix)
tmaxRequest timeout in milliseconds

Placement Parameters

These parameters define WHERE the ad appears and WHICH format to serve.

ParameterDescription
site.idYour site identifier in Ring DAS
site.ext.areaPage type context: listing, search, product_card, main, other
imp[].tagidFixed value brand_store for this format

Targeting Parameters

ParameterDescription
ext.keyvalues.manufacturer_idManufacturer identifier for targeting

Consent & Privacy Parameters

ParameterDescription
user.ext.npaConsent flag: false = consent given, true = no consent
regs.gdprGDPR applies flag: 1 = GDPR applies, 0 = does not apply
regs.ext.dsaSet to 1 to request DSA transparency info in response

User Identifier

ParameterDescription
user.ext.ids.luUser 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 Content with an empty response body. Always check the status code before parsing JSON.


Step 2: Parse Response

Extract tracking data from the bid response.

FieldDescription
gctxGlobal context - required for impression/viewability tracking
lctxLocal context - required for impression/viewability tracking
meta.adclickClick tracking base URL
fields.clickClick parameter to append to tracking URL
bid.ext.dsaDSA 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 URL

Step 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

ParameterDescription
eventDataURL-encoded JSON with gctx and ems array
gctxGlobal context from bid response
lctxLocal context from bid response
is_measurableSet 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 /av only 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: UIView visibility checks + CADisplayLink or Timer for duration
  • Android: View.getGlobalVisibleRect() + Handler for 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.

ParameterDescription
user.ext.ids.luValue 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 lu on 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


What’s Next

Learn about the web-based Brand Store integration