Skip to main content

Serving Ads in an App

Rollout status

This page describes the mobile ad contract, contract_version: 1. Availability is enabled per placement — confirm with the MageAds team which of your placement codes already serve it before you start.

How it works

Your app knows one thing about an ad slot: its placement code. It does not know in advance what kind of ad will come back — a product listing, a banner image, or a rich media creative. That is decided by the auction, per request.

So the response tells you. Every ad carries a render field, and your app branches on that one value:

renderWhat arrivedWhat the app does
nativeproduct identifiersRenders them with your own product cards
asseta ready imageDisplays it, whole tile tappable
mediaa rich creativeFetches its content and composes it natively
unsupportedsomething your app cannot showCollapses the slot, calls blockedTag

Never branch on the placement code or on type. The same placement returns different kinds of ads on different requests, and new formats are added on our side over time. Branching on render means an app already in the stores keeps working when that happens.

Requesting an ad

POST https://delivery.mageads.com/products?pc={PLACEMENT_CODE}&output=json

Send this from your backend, not from the app — the request needs an API key, and anything compiled into an app binary can be extracted from it. Your backend adds the key, forwards the request, and passes the response back. It may reshape it freely.

Recommended timeout: 400–500 ms. Render the screen first and fill the slot when the response arrives. No fill means collapse the slot completely — never leave an empty frame.

The request is the same for every format

You do not request a render mode — you request a placement. The same call can come back as native, asset or media depending on what wins the auction. Only the site block changes, and only because the screen differs.

POST /products?pc=30703746&output=json HTTP/1.1
Host: delivery.mageads.com
Authorization: Bearer <apikey>
Content-Type: application/json
Accept: application/json

Home screen

{"id": "home_1786090451", "number_of_prods": 6,
"user": {"id": "MjM1YzVmODQtMDBmOS00NDg2LWEwMGYtMTNlZjlmZmM1YjAw"},
"site": {"page": "app://home"},
"device": {"w": 1080, "h": 400}}

Category / listing

{"id": "category_1786090451", "number_of_prods": 6,
"user": {"id": "MjM1YzVmODQtMDBmOS00NDg2LWEwMGYtMTNlZjlmZmM1YjAw"},
"site": {"page": "app://category/sneakers", "cat": ["Obuwie", "Sneakersy"]},
"device": {"w": 1080, "h": 400}}

Product detail

{"id": "product_1786090451", "number_of_prods": 4,
"user": {"id": "MjM1YzVmODQtMDBmOS00NDg2LWEwMGYtMTNlZjlmZmM1YjAw"},
"site": {"page": "app://product/5901234567890", "gtin": "5901234567890"},
"device": {"w": 1080, "h": 400}}

Search results

{"id": "search_1786090451", "number_of_prods": 3,
"user": {"id": "MjM1YzVmODQtMDBmOS00NDg2LWEwMGYtMTNlZjlmZmM1YjAw"},
"site": {"page": "app://search", "keyword": "sneakersy"},
"device": {"w": 1080, "h": 400}}

Request fields

FieldTypeRequiredWhat it does
idstringYour request identifier; echoed back so you can match response to request
number_of_prodsint✅ for product formatsHow many products to return. Match it to slots when the creative carries tiles
user.idstringAdvertising identifier, base64url encoded. Ties the request to the profile your SDK builds
site.pagestringScreen identifier. Free-form; use a stable value per screen
site.catarrayCategory path, most specific last. Drives category targeting
site.keywordstringSearch query, verbatim. Drives keyword targeting
site.gtinstringProduct being viewed, on product screens. Drives related-product selection
device.w / device.hintAd slot size in pixels, not screen size. Used to match creative dimensions
device.ua / device.ipstringPassed by your backend for fraud and geo signals
bvendorarrayBrands to exclude from the response

The one addition for apps: send the advertising identifier as user.id, the same one your app reports through the Mobile SDK. Profiles are matched on that value.

Where to request ads

A placement code is bound to one screen type. Ask MageAds for a separate code per screen — using a home-page code on a search screen returns poor results, because the ad decision has no context to work with.

Screen in your appPlacement contextWhat to send in site
Home / starthome_pagepage
Category, listing, brand pagecategory_pagepage, cat[] — most important category first
Product detailproduct_pagepage, gtin of the product being viewed
Search resultssearch_pagepage, keyword — the user's query, verbatim

The same screen can host two kinds of ad: a sponsored products placement and a display placement. They are separate codes and separate requests.

This is the format with the strongest commercial performance, and the one most sensitive to getting the request right.

{
"id": "search_1786090451",
"number_of_prods": 3,
"user": {"id": "MjM1YzVmODQtMDBmOS00NDg2LWEwMGYtMTNlZjlmZmM1YjAw"},
"site": {"page": "app://search", "keyword": "sneakers", "cat": []},
"device": {"w": 1080, "h": 400},
"bvendor": []
}

Rules that matter:

  • Send keyword exactly as the user typed it. Do not stem it, translate it or strip diacritics — matching happens on our side and normalising twice loses intent.
  • Request the ad in parallel with your own search, not after it. The ad response and the organic results are independent; waiting for one before starting the other doubles the delay.
  • Never let ads delay the results. If the ad response has not arrived by the time you render, render without it. Filling the slot slightly later is acceptable; an empty screen is not.
  • Decide the positions in advance and keep them stable. Sponsored items are usually placed at the top of the list or at fixed positions inside it. Whatever you choose, keep the returned pos order among them.
  • Deduplicate against organic results. If a sponsored product also appears in your own results, show it once — as the sponsored one — and drop the organic occurrence, otherwise the user sees the same product twice.
  • Label them. In search results the disclosure matters more than anywhere else, because the user reads the list as neutral.

Everything else — resolving gtin in your catalogue, pixels, ordering — works exactly as described under render: native.

Same contract, different context field:

  • Category — send the full category path in cat[], most specific last. The listing is the natural place for a sponsored strip or for items interleaved into the grid.
  • Product detail — send gtin. Results are related or alternative products; place them in the "similar products" area rather than above the fold.
  • Home — send page only. Results are a general recommendation; a horizontal strip works best.

render: native — product listings

The ad is a list of products from your own catalogue, promoted in a chosen order.

{
"id": "category_1786090451",
"contract_version": 1,
"banners": [{
"render": "native",
"sponsored_products": [
{"gtin": "5901234567890", "pos": 1,
"pixel_view": "https://delivery.mageads.com/…/v.gif?…&prid=5901234567890",
"pixel_click_nordr": "https://delivery.mageads.com/…/c.gif?…&nordr=1",
"advertiser": "Brand X",
"optout": "https://optout.mageads.com/?dsa=1&dsa_behalf=Brand+X&muid=…"}
]
}]
}
FieldWhat it does
gtinProduct identifier in your catalogue — resolve it there for name, price and image
posPosition in the promoted order, 1 first. Keep it
pixel_viewImpression pixel for this product. Fire when its card becomes visible
pixel_click_nordrClick pixel that counts and returns immediately. Preferred in apps
pixel_clickClick pixel that redirects to the product page. For browsers
advertiserAdvertiser name. Use it in the disclosure label
optoutDSA opt-out URL for this ad. Expose it where your disclosure UI allows

Implementation. Resolve each gtin in your catalogue and render it with the product card you already use. Keep the returned order — position is part of the ad decision. Label every item as advertising and expose the optout link where your disclosure UI allows. If a product is missing or out of stock, drop it and render the rest; never substitute another product.

Fire pixel_view when the card becomes visible and pixel_click_nordr on tap.

render: asset — image creatives

The creative is a finished image. The whole tile is the click target; any call-to-action button is part of the artwork.

{
"render": "asset",
"creative": {
"url": "https://static.mageads.com/c/18554/b/231001/r/1080w.webp",
"w": 1080, "h": 900,
"aspect_ratio": "6 / 5",
"alt": "Shop ABC — advertisement"
},
"click": {
"pixel": "https://delivery.mageads.com/…/c.gif?…&nordr=1",
"url": "https://shop.example.com/collection",
"url_app": "myshop://category/sneakers"
},
"counter": "…", "visibilityTag": "…", "blockedTag": "…"
}
FieldWhat it does
creative.urlThe image to display
creative.w / creative.hPixel size of the file, not of your slot
creative.aspect_ratioDesign proportion. Size your slot from this, not from w/h
creative.altAccessibility label. Set it as the view's content description
click.pixelClick pixel. Fire on tap, before navigating
click.urlWeb landing page
click.url_appDeeplink into your app. Use it when present and resolvable, else click.url

Implementation. Size the slot from aspect_ratio — take the width from your layout and derive the height. Never size from w/h; those describe the asset, not your slot. Load the image, set alt as the accessibility label, make the whole view tappable.

On tap: fire click.pixel, then open click.url_app if present and resolvable, otherwise click.url.

Image creatives with products

Some image creatives carry product tiles. You get the background and the tiles as separate images, and the product data alongside them:

{
"render": "asset",
"slots": 8,
"background": {"url": "…/bg/1080w.webp", "aspect_ratio": "6 / 5", "alt": "Shop ABC — advertisement"},
"tiles": [
{
"sku": "5901234567890", "pos": 1,
"image": {"url": "…/t/5901234567890/360w.webp", "aspect_ratio": "3 / 4",
"alt": "Sneakers X, 299.00 PLN — advertisement"},
"data": {"name": "Sneakers X", "price": "299.00", "old_price": "399.00",
"currency": "PLN", "image_url": "https://…/prod.jpg",
"url": "https://shop…/product"},
"click": {"pixel": "…/c.gif?…&prid=5901234567890&nordr=1", "url": "https://shop…/product"},
"pixel_view": "…/v.gif?…&prid=5901234567890"
}
],
"counter": "…", "visibilityTag": "…", "blockedTag": "…"
}
FieldWhat it does
slotsHow many products the creative was designed for
backgroundBrand layer of the creative — same fields as creative above
tiles[].skuProduct identifier in your catalogue
tiles[].posPosition in the promoted order
tiles[].imageOur rendering of this tile, if you want the creative as designed
tiles[].dataThe same product as data, if you prefer your own card
tiles[].clickClick pixel and destination for this product
tiles[].pixel_viewImpression pixel for this product

You choose how to render each tile. Use our image if you want the creative exactly as designed, or use data to build the tile with your own product card. Either way the tracking fields are per product, so attribution works identically — and how you arrange the tiles, a horizontal strip or a grid, is entirely your decision.

slots is how many products the creative was designed for. Ask for that many in number_of_prods or some tiles will stay empty.

render: media — rich creatives

Rich creatives are video and image carousels with a product strip. They are not flattened, because flattening would throw away what the format is. Instead you get a reference to the creative's content:

{
"render": "media",
"creative_url": "https://api-panel.mageads.com/shopable-media/360/render.json/mobile",
"aspect_ratio": "1240 / 450",
"counter": "…", "visibilityTag": "…", "clickTagRaw": "…", "blockedTag": "…"
}

Fetch creative_urlfrom your backend, which can cache it aggressively; creative content changes far less often than ad decisions:

{
"contract_version": 1,
"marketing_title": "Autumn 2026 collection",
"marketing_description": "New models available online",
"marketing_cta_text": "See the collection",
"marketing_target_url": "https://shop…",
"content_media": [
{"type": "video", "url": "https://stream.mux.com/….m3u8", "order": 1},
{"type": "image", "url": "https://…/frame-2.jpg", "order": 2}
],
"logo_media": {"type": "image", "url": "https://…/logo.png"},
"products": [
{"sku": "5901234567890", "name": "Sneakers X", "price": "299.00",
"url": "https://shop…", "image_url": "https://…/prod.jpg"}
]
}
FieldWhat it does
marketing_title / marketing_descriptionHeadline and body copy
marketing_cta_textLabel for the call-to-action button
marketing_target_urlWhere the call-to-action leads
content_media[].typevideo or image
content_media[].urlMedia file. Videos are HLS streams
content_media[].orderDisplay order in the carousel
logo_mediaAdvertiser logo
products[]Products to show in the strip: sku, name, price, url, image_url

Implementation. A pager or carousel over content_media, ordered by order. Images load directly; videos are HLS streams that play natively — AVPlayer on iOS, ExoPlayer on Android, no extra library. Below or beside it, a strip of product cards from products. The CTA is a button with marketing_cta_text opening marketing_target_url.

Size the slot from aspect_ratio, as with image creatives.

Keep the second request on your backend

If your backend fetches creative_url and merges the result into its response, your app sees one shape for every ad and never makes a second call. That is the simplest integration and it is what we recommend.

render: unsupported

The auction returned a creative your app has no way to display. Collapse the slot and call blockedTag. Do not show a placeholder.

This is also the safe default: if you receive a render value you do not recognise — because a new format was added — treat it as unsupported.

Measurement

On the web our JavaScript library decides when an ad was seen. In an app you decide, and the quality of the reporting depends entirely on getting it right. The rules are identical across all render modes.

EventFieldWhen
Impressioncounter, or pixel_view per productThe ad is rendered on screen
ViewablevisibilityTagAt least 50 % of the ad area is visible
Clickclick.pixel / clickTagRaw / pixel_click_nordrThe user taps
BlockedblockedTagThe creative arrived but could not be displayed

Fields that control measurement

FieldWhat it does
counterImpression pixel for the ad as a whole
nocounter1 means the impression is counted elsewhere — do not call counter
visibility_check0 means viewability is not measured for this creative
visibilityTagViewability pixel. Absent when visibility_check is 0
clickTagRawClick URL, unescaped. clickTag is the same value HTML-escaped — use the raw one
blockedTagCall when the creative arrived but could not be displayed
upixelCall when a rendered creative is withdrawn before it could be seen

The viewability pixel carries its measurement in the query string. A bare call records nothing. The first call must include &views=1&time=<seconds> — that is what makes the impression viewable. Every later call includes only &time=<seconds since the previous call> and accumulates dwell time. Values outside 0.5–300 seconds are discarded, and views=1 must never be sent twice for the same appearance.

visibilityTag is absent when visibility_check is 0 — that creative is not measured for viewability. Likewise, skip counter when nocounter is 1.

Take clicks without the redirect. Append &nordr=1 so delivery counts the click and returns immediately, then open the landing page yourself. Fields ending in _nordr already include it.

Deduplicate per appearance. One impression, one views=1, at most one click. Recycled cells in a scrolling list must not re-fire pixels for an ad they already reported — the most common source of inflated numbers in app integrations.

Call the URLs exactly as returned, query string intact. They carry the identifiers tying the event to its ad request; a rebuilt or double-escaped URL is discarded.

Verifying the integration

A successful HTTP response does not mean the impression was counted. Every pixel answers 200 regardless of what happened server-side. An impression can arrive and still be ignored — disqualified because a campaign is inactive or a placement is not assigned to it. From the app this is invisible.

So verification ends with the reports:

  1. Confirm the pixels leave the device with query strings unmodified.
  2. Count locally how many times you fired each one.
  3. Ask MageAds for that placement's figures for the same day — impressions counted, ignored, and viewable.

More ignored than counted points at campaign configuration. Fewer fired than expected points at the app missing renders — usually a recycled list cell deduplicated when it should not have been.

Checklist

  • Ad requests go through your backend; the API key is not in the app binary
  • The app branches on render, never on the placement code or type
  • An unrecognised render value is treated as unsupported
  • user.id carries the same advertising identifier the SDK reports
  • Slots are sized from aspect_ratio
  • Sponsored products are labelled as advertising and keep their returned order
  • number_of_prods matches slots for creatives that carry product tiles
  • Pixels are called from the device, query strings intact
  • First visibilityTag call carries views=1; later calls carry only time
  • counter skipped when nocounter is 1; visibilityTag skipped when absent
  • Clicks use nordr=1; the app opens url_app or url itself
  • Pixels deduplicated per appearance, including recycled list cells
  • Unsupported creatives collapse the slot and call blockedTag