QiAds
v1.3.1Stable release≈ 15 KB compressed

QiAds Web SDKDeveloper guide

One script that shows the ads live on QiAds placements in any page or in-app WebView, and measures ad requests, rendered and viewable impressions and clicks to MRC definitions — no cookies, no visitor identity.

  • Two lines to install

    One script tag and one slot element; no libraries, no build step.

  • Measured to standard

    Ad requests, rendered impressions, viewable (50% for 1 s) and server-side clicks.

  • Privacy first

    No cookies, no visitor identity; aggregate counts per Baghdad day.

  • Built for apps

    Events for the host app, click handoff, Android and iOS WebView support.

Installation

Pick one of the two forms. Each placement's code, at its size, is in «كود التضمين» in the admin console.

EvergreenRecommended

Always the latest 1.x release; every fix arrives within minutes.

HTML
<script async src="https://qi-ads.com/qiads.js" data-site="superqi"></script>

Pinned with SRI/sdk/1.3.1/qiads.js

A file that never changes, with its hash from the release itself — for security reviews that require it.

HTML
<script async src="https://qi-ads.com/sdk/1.3.1/qiads.js"
        integrity="sha384-BWO68+7tX2tlBmx9ZTWFyWudoNINILOXW2YLBQndvFTmi/2GNrcDff5/ddQfMPRa"
        crossorigin="anonymous" data-site="superqi"></script>

data-site is the channel id: superqi or gini.

Contents · 19 sections
1

Overview

qiads.js is one dependency-free script that shows the ads that are live right now on a QiAds placement — a rotating banner, an inline card or a popup — on any web page or inside an app's WebView, and measures them. It is served from the same origin as the ad API: https://qi-ads.com.

What it measures (aggregate counts per Baghdad day; full definitions in Measurement methodology):

  • Ad requests and fill: each time a slot asks for its ads — every load of a screen that carries the slot.
  • Rendered impressions: the ad was inserted and its image finished loading.
  • Viewable impressions: 50% of the ad on screen for one continuous second (MRC).
  • Clicks: counted by the QiAds server as it redirects the user to the advertiser.
  • Daily unique devices (estimate): an approximate count of devices, never of people.

What it never does: set or read cookies (one exception: the popup frequency cap on a page without DOM storage, see Privacy), use persistent identifiers, fingerprint the device, collect names, phone numbers or accounts, or send the part of the page URL after ?.

Each ad renders inside Shadow DOM (a <qiads-slot> element in the slot, <qiads-popup> for the popup): your page's styles cannot reach it and it changes nothing else on the page. Engines without Shadow DOM get the ad in the page with styles scoped to .qiads* classes.

2

Quick start

HTML
<!-- 1) Once per page, before </body> -->
<script async src="https://qi-ads.com/qiads.js" data-site="superqi"></script>

<!-- 2) Where the ad goes: the space is reserved before it arrives -->
<div data-qiads="superqi_homepage_slot" data-format="banner" data-aspect="9/4" style="aspect-ratio:9/4"></div>
  • data-qiads is the placement id as the admin console's «كود التضمين» lists it, e.g. superqi_homepage_slot for the SuperQi home slider.
  • data-site is the id of the channel showing the ad: superqi or gini. Anything else is reported as "other".
  • style="aspect-ratio:9/4" and data-aspect reserve the ad's box at the placement's proportions (1080×480 = 9:4), so nothing jumps when it appears.
  • Only a booking that is live, inside its days and with a valid image is shown; otherwise the slot collapses (see data-collapse).
  • Before going live, try it in test mode: add ?qiads_test=1 to the page URL — everything renders, nothing is counted.
3

Installation

HTML
<script async src="https://qi-ads.com/qiads.js" data-site="superqi"></script>

/qiads.js is always the latest 1.x release. It is cached for 5 minutes and revalidated in the background for up to a day, so a fix reaches every page within minutes without any change on your side. No 1.x release breaks what works today (see Versioning policy).

Pinned with Subresource Integrity

HTML
<script async src="https://qi-ads.com/sdk/1.3.1/qiads.js"
        integrity="sha384-…" crossorigin="anonymous" data-site="superqi"></script>

/sdk/<version>/qiads.js never changes (cached for a year, immutable), so it can be pinned with an SRI hash: the browser refuses the file if a single byte differs. The exact integrity value is printed on the developer guide page and on the admin console's «كود التضمين», from the release file itself. Upgrading is your decision: change the version and the hash together. crossorigin="anonymous" is required for SRI, and the server sends Access-Control-Allow-Origin: * for these files. The previous release stays available for rollback: /sdk/1.2.1/qiads.js.

Loading

  • Always async. The tag may go in <head> or before </body>; commands issued before it loads wait in a queue (JavaScript API).
  • For a faster first ad: <link rel="preconnect" href="https://qi-ads.com" crossorigin>.
  • Including the tag twice is harmless: the second copy does not start a second SDK.
  • When you host the file on another origin, set data-host="https://qi-ads.com".
4

In-app WebViews

The tag runs unchanged inside Android WebView and iOS WKWebView, but the host app owns three things: where clicks open, when the screen is really visible, and which origins it trusts.

WebView settings

  • Android: settings.javaScriptEnabled = true and settings.domStorageEnabled = true (the popup frequency cap lives there; without it the SDK falls back to a cookie on your page's domain). Load the page from its https:// URL with loadUrl, not loadData or local files, so the page has a real origin and address. Third-party cookies are not needed.
  • iOS: the WKWebView defaults are enough (JavaScript is on). Implement the click delegates below.
  • For a local page (file:// or a custom scheme) set data-host="https://qi-ads.com", and use data-page for a logical screen name such as superqi/home.
  • If your screens' paths carry identifiers (a phone, account or order number, or a token), use data-page with a logical screen name too. The server replaces long numbers and tokens in a path with :id, but a logical name reads better in the reports and carries nothing about the user.

Clicks: hand them to the app

Never set data-target="_self" inside an app: the advertiser's page would load in the app's own WebView, next to whatever JavaScript bridge the app exposes. The default _blank is not enough either: on Android (with the default setSupportMultipleWindows(false)) it becomes a navigation that replaces your screen, and WKWebView does nothing unless createWebViewWith is implemented. Take the click instead:

JS
document.addEventListener("qiads:click", function (event) {
  var bridge = window.SuperQiBridge                     // your app's own bridge object
  if (!bridge || !bridge.openExternal || !event.detail.url) return   // no bridge: the link opens as usual
  event.preventDefault()                                // the SDK does not navigate
  bridge.openExternal(event.detail.url)                 // the app opens it in Custom Tabs / SFSafariViewController
})

Cancel only when the bridge is there and there is a URL to hand over: cancelling and then failing to reach the bridge (the page opened in a plain browser, an older app build) loses the click — no navigation and no count. Or through the configuration: QiAds.init({ onClick: function (detail) { if (window.SuperQiBridge && detail.url) { SuperQiBridge.openExternal(detail.url); return false } } }).

detail.url is the QiAds click redirect (https://qi-ads.com/api/ads/click/…). Open it in an in-app browser — Custom Tabs on Android, SFSafariViewController on iOS — and the click is counted when that browser follows the redirect to the advertiser. Never resolve the link with an HTTP client first (OkHttp, URLSession): that click is not the user's, it is classified as a bot, and the user loses the landing page.

When a click does not go through the bridge, intercept the navigation natively:

KOTLIN
webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
        val url = request.url
        if (url.host == "your-own-host.example") return false   // your pages stay in the WebView
        CustomTabsIntent.Builder().build().launchUrl(view.context, url)
        return true
    }
}
SWIFT
// WKUIDelegate: _blank links
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
             for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
    if let url = navigationAction.request.url { present(SFSafariViewController(url: url), animated: true) }
    return nil
}

// WKNavigationDelegate: every tap on an outside link
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if navigationAction.navigationType == .linkActivated,
       let url = navigationAction.request.url, url.host != "your-own-host.example" {
        present(SFSafariViewController(url: url), animated: true)
        decisionHandler(.cancel)
        return
    }
    decisionHandler(.allow)
}

Restrict the JavaScript bridge to your own origins: WebViewCompat.addWebMessageListener with allowedOriginRules on Android, a check of message.frameInfo.securityOrigin on iOS. A long-press link preview on iOS opens the link without a click event — the click is still counted, because the redirect goes through the QiAds server.

Lifecycle: visibility and refresh

  • Android: call webView.onPause() and webView.onResume() with the activity; document.visibilityState then follows the screen and nothing is measured while it is hidden.
  • A native screen drawn over a WebView that stays attached does not make the page hidden. Tell the SDK: QiAds.setVisible(false) when it is covered, QiAds.setVisible(true) when it is back (through evaluateJavascript or evaluateJavaScript). While it is false, viewability timers and rotation stop.
  • Long-lived screens: when the user comes back to a screen that stayed in memory (a home tab), call QiAds.refresh() to start new impressions with fresh ads. The SDK refreshes its slots by itself after 30 minutes hidden and when the Baghdad day changes.
  • Single-page apps: after a route renders new slots, call QiAds.init(), or add data-observe to the tag to fill them automatically.

Content-sized WebViews (known limit)

If the WebView is laid out at the full height of its content inside a native scroll view, the page cannot know what is on screen: IntersectionObserver sees every slot as visible, and viewable impressions are overstated. Keep the WebView screen-sized (it scrolls its own content), or tell us — an API for the app to pass its scroll position is on the roadmap.

Allowlists and CSP

Allow https://qi-ads.com for: the script, connections (serve and track), images (the creatives) and navigation (the click redirect /api/ads/click/…, which then sends the user to the advertiser's domain). Mini-program domain lists need the same origin. A CSP example: script-src https://qi-ads.com; connect-src https://qi-ads.com; img-src https://qi-ads.com.

Checklist on real devices

  1. The test build carries data-test (or ?qiads_test=1): ads render and nothing is counted.
  2. Debugging: ?qiads_debug=1, then the console through chrome://inspect or Safari Web Inspector shows every step and the server's X-QiAds-Result verdict.
  3. Tapping an ad opens it in Custom Tabs or SFSafariViewController and the app stays where it was; Back returns to the app.
  4. Popup: the Android Back button closes it (map Back to QiAds.close() while it is open).
  5. Covering the WebView with a native screen stops viewable impressions; coming back resumes them.
  6. Send us the exact User-Agent strings of the app's WebView on Android and iOS (we pin them in our invalid-traffic tests).
  7. Confirm the WebView is screen-sized, not content-sized.
5

Native and server integration

For native screens with no WebView, use the REST API:

  • Request GET /api/ads/serve and show the image, then report with POST /api/ads/track (version 2) from the device itself, with a User-Agent containing QiAdsNative/, e.g. QiAdsNative/1.0 (android; app=superqi/5.2). Generic HTTP client user agents (okhttp/4.12.0) are classified as bots.
  • The viewability rule you apply yourself: at least 50% of the ad's pixels on screen for one continuous second while the app is in the foreground, once per impression id (i: 12 random characters of [a-z0-9], new for every showing of the screen × creative).
  • Send request (with n), render (with w and h, and mb:1 when you measure visibility) and viewable. Without render your viewable impressions stay out of the viewability rate. Do not send click when the creative carries click_href (it is excluded as not_navigation): open click_href in an in-app browser and the redirect counts it.
  • Never resolve click_href with an HTTP client before opening it.
  • Send test:1 during development.
6

Configuration reference

Script tag attributes

AttributeSinceDefaultMeaning
data-host1.2.1the script's originAPI origin when the script is hosted elsewhere
data-site1.2.1emptyThe channel id: superqi or gini; anything else is reported as "other" (up to 64 characters)
data-lang1.2.1<html lang>, else arAd language for the page: ar, en or ku
data-popup1.2.1—Opens a popup after load. Kept for compatibility only; do not use it — open popups at a natural transition point with QiAds.popup
data-cap1.2.11/dayPopup frequency cap: 1/day, 1/session or always
data-rotate1.2.17000Milliseconds between creatives; minimum 3000 (smaller values are raised to it), 0 stops rotation
data-test1.3.0offTest mode: renders normally, nothing is counted
data-debug1.3.0offDebug mode: console.info diagnostics and the server's X-QiAds-Result verdict
data-observe1.3.0offFills [data-qiads] elements added to the page later (MutationObserver)
data-page1.3.0the page URLA logical page name reported instead of the URL, e.g. superqi/home (Latin letters, digits and _-/., up to 128)

Boolean attributes (data-test, data-debug, data-observe) are on when present; the values 0, false, no and off turn them off.

Slot attributes

AttributeAliasSinceDefaultMeaning
data-qiads—1.2.1(required)The placement id, [a-z0-9_]{2,64}
data-formatdata-qiads-format1.2.1bannerbanner rotating banner · inline card · popup
data-limitdata-qiads-limit1.2.17Most creatives a banner rotates through (1–10). Cards and popups show one creative picked at random
data-rotatedata-qiads-rotate1.2.1from the tagThis slot's rotation interval
data-targetdata-qiads-target1.2.1_blank_blank or _self. Never _self inside an app's WebView (Clicks)
data-fallbackdata-qiads-fallback1.2.1—Keeps the element's own content when there is no ad
data-lang—1.2.1from the tagThis slot's language: ar, en or ku
data-dirdata-qiads-dir1.2.1by languagertl, ltr or auto
data-capdata-qiads-cap1.2.1from the tagA popup slot's frequency cap
data-aspectdata-qiads-aspect1.3.0—Reserves the space while loading at a W/H ratio, e.g. 9/4
data-collapsedata-qiads-collapse1.3.0autoauto collapses an empty slot; never keeps its reserved box
data-pagedata-qiads-page1.3.0from the tagA logical page name for this slot's events

The data-qiads-* alias is read before the short name — useful when a short name clashes with a framework that uses data-format, for example. data-lang has no alias.

Attributes the SDK writes (outputs)

AttributeValues
data-qiads-stateloading → ready, empty or error
data-qiads-langThe language actually shown (ar, en or ku) once ready

Do not set them yourself or read them as inputs: that is why data-lang has no data-qiads-lang alias — if it had one, the SDK would read back its own output (the Arabic fallback) on refresh and pin it.

window.qiadsConfig and QiAds.init(options)

OptionMeaning
hostAs data-host
siteAs data-site
langAs data-lang
testtrue turns test mode on
debugtrue turns debug mode on
onClickCalled with the click's detail before every navigation; returning false cancels it

Precedence: window.qiadsConfig (read at load) ← the tag's data-* attributes win over it ← QiAds.init(options) wins over both from the moment it is called. ?qiads_test=1 and ?qiads_debug=1 in the page URL also turn the two modes on.

7

JavaScript API

JS
QiAds.version                          // "1.3.1"
QiAds.config                           // the current configuration (read only)
QiAds.init(options?)                   // reads the configuration and fills every new [data-qiads]
QiAds.render(target, options?)         // Promise<boolean> — target: an element or a CSS selector
QiAds.popup(placement, options?)       // Promise<boolean>
QiAds.close()                          // closes the open popup
QiAds.destroy(target?)                 // one slot, or with no argument every slot and the popup
QiAds.on(type, fn)                     // listens to an event (see Events)
QiAds.off(type, fn)                    // stops listening
QiAds.refresh(target?)                 // Promise<boolean> — a new impression: new ids, fresh ads
QiAds.setVisible(visible)              // the host app's visibility: false stops measuring and rotation
QiAds.getSlots()                       // the slots on the page: {el, placement, format, state, lang, creatives}
QiAds.getVersion()                     // "1.3.1"

render options: placement, format, limit, rotate, target, fallback, lang, dir, cap, aspect, collapse and page — each option wins over the matching slot attribute.

The command queue works before the script loads and runs once the page is parsed:

JS
window.qiads = window.qiads || []
qiads.push(["render", "#slot", { placement: "superqi_homepage_slot" }])
qiads.push(["setVisible", false])
qiads.push(function (api) { api.on("impression", function (d) { console.log(d.creative.id) }) })

// or GPT-style:
window.QiAds = window.QiAds || { cmd: [] }
QiAds.cmd.push(function () { QiAds.refresh() })

Each command is its method's name followed by its arguments; a function in the queue is called with the API. After loading, window.qiads and QiAds.cmd become objects whose push runs the command at once. Every method catches its own errors: an SDK error never reaches your page.

8

Events

Every event arrives two ways with the same detail: QiAds.on(type, fn), and a DOM event named qiads:<type> that bubbles from the slot element (from document for a popup opened without a slot).

QiAds.onDOM eventWhenCancelable
requestqiads:requestThe slot asks the server for its adsno
renderqiads:renderA creative was inserted and its image loaded (a rendered impression)no
emptyqiads:emptyNothing to show; reason: no_fill, unknown_placement or invalid_placementno
errorqiads:errorThe slot could not be filled; reason: timeout, http, rate_limited, network or images_failedno
impressionqiads:impressionA viewable impression: 50% for one continuous secondno
clickqiads:clickA real click by the user, before navigationyes
closeqiads:closeThe popup was closedno

Order for one creative: request → render → impression, then click and close when they happen; empty or error instead of render when the slot cannot be filled.

The detail (event.detail, or the listener's argument):

FieldMeaning
typeThe event name
placementThe placement id
creative{id, booking_id, lang} of the creative concerned, or null
impressionIdThe impression id (one slot × one creative), or null
urlThe link a click opens: click_href with the SDK's parameters, else click_url
landingThe advertiser's own link (click_url)
slotThe slot element, or null for a popup without one
reasonFor empty and error only

A click is cancelled by any of: event.preventDefault() on the DOM event, detail.preventDefault(), or returning false from an on("click") listener or from onClick. The SDK then does not navigate, and the app opens detail.url itself, so the click is still counted. A click the browser does not mark as real (isTrusted false, such as a script's el.click()) neither navigates nor fires events.

JS
QiAds.on("empty", function (d) { document.querySelector("#promo").hidden = true })
QiAds.on("impression", function (d) { analytics.track("ad_viewable", { placement: d.placement }) })
9

Formats, sizes and reserved space

  • banner: an image as wide as the slot. With several live campaigns it starts on a random creative on every load and rotates every data-rotate, with a visible pause/play button (WCAG 2.2.2), navigation dots and touch swipe that follows the language's direction. Rotation pauses on keyboard focus and while the pointer is over it, and while it is off screen or the page is hidden; a passing tap does not stop it. With "reduce motion" it keeps rotating without transitions. The visible creative loads at once, the others later.
  • inline: a notification-like card (small image, title and text) with one creative.
  • popup: an overlay with a close button that closes on Escape or a click outside and returns focus where it was. It opens only once its image has loaded. Open it at a natural transition point — after a completed action, when leaving a screen — not when the app opens: qiads.push(["popup", "superqi_popup"]); the default frequency cap is once a day.

Reserving space (no layout shift): reserve the slot's box at the placement's proportions before the ad arrives. The simplest way is style="aspect-ratio:9/4" on the slot; if your page's CSP refuses style attributes, put it in your stylesheet: [data-qiads="superqi_homepage_slot"]{aspect-ratio:9/4}. Add data-aspect="9/4" so the SDK reserves the space itself in engines without CSS aspect-ratio (a padding-top fallback). Once the server answers, the box takes the placement's actual proportions.

Collapsing: with no ad the slot is hidden (display:none) by default. data-fallback keeps its own content (a house ad, for example), and data-collapse="never" keeps the reserved box with a neutral background so the content below does not move.

10

Languages

Every creative has an Arabic version; an advertiser may add an English and/or a Kurdish one. The landing link is the same for every language. A slot's language is the first recognised value of: lang in QiAds.render, then data-lang on the slot; then data-lang on the tag; then <html lang> (en* → en · ku, ckb*, kmr* → ku · ar* → ar); otherwise ar. An unrecognised value is skipped.

Fallback rule: the requested language's version is shown when an image was uploaded for it, otherwise the same campaign's Arabic version; a campaign never disappears for lack of a translation, and two languages are never mixed in one ad. data-qiads-lang on the slot and creatives[].lang in the serve answer show the language actually served. Direction follows the language shown (rtl for Arabic and Kurdish) unless data-dir is set, and so does the «Ad» badge (إعلان · Ad · ڕیکلام). Figures count per creative, whatever the language.

11

Measurement methodology

The terms follow the MRC and IAB guidelines, and they are the same texts the reports and the admin console show:

  • Day: All figures are by Baghdad calendar day (UTC+3), midnight to midnight.
  • Ad requests: Ad request: each time a slot asks the ad server for its ads — one load of a page or screen that carries the slot. Fill rate = requests that returned at least one ad ÷ all requests.
  • Rendered impressions: Rendered impression: the ad was inserted into the page and its image finished loading (begin-to-render, counted in the browser), once per impression.
  • Viewable impressions: Viewable impression: at least 50% of the ad's pixels on screen for one continuous second, with the page visible and the image loaded, after its fade-in; once per impression (MRC definition).
  • Impression: Impression: one ad in one slot since the slot loaded. The same ad coming round again in the rotation is not counted again; reloading the slot (in-app navigation or QiAds.refresh) starts a new impression.
  • Viewability rate: Viewability rate = viewable impressions measured by SDK 1.3 or later ÷ rendered impressions, over the days with rendered impressions only. 'Measurement incomplete' marks a day on which some viewable impressions were counted by a tag older than 1.3 (it sends no renders, so they stay out of the rate), or a day with fewer renders than those viewable impressions, left out of the rate.
  • Measured rate: Measured rate = rendered impressions whose viewability could be measured (the browser can observe element visibility and page visibility) ÷ all rendered impressions.
  • Clicks: Click: counted on the QiAds server when it redirects the user to the advertiser's link, once per device and ad within 30 seconds. With tags older than 1.3 the browser reports the click instead.
  • CTR: CTR = clicks ÷ viewable impressions.
  • Reach: Daily unique devices (estimate): an approximate count of distinct devices in one day, from a keyed hash of the network address and browser whose daily key is deleted after two days. A device is not a person, and devices sharing a network address and phone model count as one, so the figure is usually low. Days are never added together.
  • Gross and net: Net = counted. Gross = net + general invalid traffic (bots, requests without a user agent, automated browsers, zero-size ads, prefetch, clicks without navigation). Test traffic, bookings not live and the daily per-device cap are 'excluded, not invalid' and shown separately.

Measurement details:

  • Visibility is measured with IntersectionObserver, and in engines without it by checking the element's position every 100 milliseconds with the same rule (a render counts as measured by that check in the top-level window only: inside a cross-origin frame it cannot see the visitor's screen). Nothing is counted for an ad without area, a hidden element (display:none, visibility:hidden or opacity:0), a hidden page or QiAds.setVisible(false), or before the fade-in ends (600 milliseconds).
  • General invalid traffic (GIVT) is excluded for these reasons: bot (a known bot or crawler), no_ua (no user agent), automation (an automated browser: HeadlessChrome or navigator.webdriver), zero_size (an ad of one pixel or less), prefetch (a speculative load of the link), not_navigation (a click that was not a navigation: the click link loaded as an image, script, fetch or frame, or a 1.3 click beacon for an ad whose clicks go through the redirect). QiAdsNative/ apps are exempt from the user-agent rules. Data-centre filtering is deferred.
  • Excluded, not invalid: test (test traffic), not_live (a booking that is not live), capped (the daily per-device cap).
  • Daily caps per device and creative: 400 viewable impressions, 30 clicks and 400 rendered impressions a day (one limit for viewable and rendered, so the cap never moves the viewability rate); anything beyond is excluded as capped. Duplicates do not count: a viewable or rendered impression once per impression within 30 minutes, a click once per device and creative within 30 seconds.
  • Events more than 15 minutes older than the moment they were sent (a WebView waking up hours later) are not counted, so yesterday's views are never filed under today.
  • Ad requests and rendered impressions are never stored as single events: they are daily counters only. The server can pause them under load through the measure field of the serve answer.
12

Privacy and data

DataWhereKept
Event kind (viewable or click), creative, booking, placement, timesingle event30 days
The site as sent (up to 64 characters)single event30 days; reports show a channel id or "other"
Page: origin and path only, or a logical name; never ? or #, and long numbers and tokens in the path replaced with :idsingle event30 days
Referrer: origin onlysingle event30 days
User agent (up to 512 characters)single event30 days, for invalid-traffic checks
A keyed hash of the network address under a random daily salt (never the address)single event30 days
SDK version and event sourcesingle event30 days
A daily per-device key and its count (reach and daily caps)separate table2 days, never backed up
The daily salt (32 random bytes)database2 days, never backed up; D1 Time Travel may keep a copy for up to 30 days
Daily counts: requests, fill, rendered, viewable, clicks, the device estimate, filtered trafficaggregatesas long as the booking exists, with nothing about the visitor
Last popup time qiads_popup_<placement>the visitor's browser, on your domaina day or a session

We do not collect: the IP address itself, cookies (the server sets none and reads none), names, phone numbers or accounts, advertising ids, the part after ? or #, the full referrer, or location. Once a day's salt is deleted after two days, that day's hashes cannot be linked to an address. The SDK keeps only the last popup time in the browser, in localStorage or sessionStorage, and when storage is disabled in a cookie of the same name on your page's domain. Cloudflare's operational logs may keep request data (address and user agent) for up to 7 days. Details: privacy policy (Arabic).

13

Test and debug mode

Test mode — data-test on the tag, ?qiads_test=1 in the page URL, or QiAds.init({ test: true }): everything renders and every event fires, but beacons carry test:1 and the click link carries t=1, so the user is redirected and nothing is counted. The preview page /ads-demo.html is always in test mode.

Debug mode — data-debug, ?qiads_debug=1, or QiAds.init({ debug: true }): logs every step with console.info (visible in chrome://inspect and Safari Web Inspector), outlines every ad, and sends with fetch and keepalive so it can read the server's verdict from the X-QiAds-Result header and print it:

ValueMeaning
countedEvery event was counted
testTest traffic: nothing was stored
duplicateEvery event was a duplicate
partialSome events were counted and some were not
filtered:<reason>Nothing was counted; the first event's reason: bot, no_ua, automation, zero_size, prefetch, not_navigation, not_live, capped, rate_limited, bad_token or stale

The header is readable from any origin (Access-Control-Expose-Headers), and the click redirect's answers carry it too.

14

Caching and versions

PathCache-ControlNote
/qiads.jspublic, max-age=300, stale-while-revalidate=86400Evergreen: a new release arrives within minutes
/sdk/<version>/qiads.jspublic, max-age=31536000, immutableNever changes; fit for SRI
/api/ads/servepublic, max-age=60, s-maxage=120, stale-while-revalidate=300A new or ended campaign shows within minutes; ttl = 60
/api/ads/asset/…shared cache for about 5 minutes, with ETagA stopped campaign's image leaves shared caches within minutes
/api/ads/click/…no-storeEvery click reaches the server

All of these are CORS-open (Access-Control-Allow-Origin: *) without credentials, with Cross-Origin-Resource-Policy: cross-origin.

15

Troubleshooting

SymptomCheck
Nothing shows; data-qiads-state="empty" with no_fillA booking that is live, today inside its days, an open channel, a valid image. Open /api/ads/serve?placement=… directly
empty with unknown_placementA wrong id, or the placement is switched off
empty with invalid_placementThe id is not [a-z0-9_]{2,64}; no request is made
error with timeout, network, http or rate_limitedReaching qi-ads.com, connect-src in the CSP, data-host
error with images_failedimg-src in the CSP, or the creative's image is unavailable (tell us)
A click opens inside the app, or nothing happens on iOSClicks: hand them to the app
Figures do not move in the dashboardTurn debug mode on and read X-QiAds-Result: test (is test mode on?), filtered:not_live, filtered:capped, filtered:bot (the user agent)
Few viewable impressions against rendered onesThe slot is below the fold, the WebView is covered, or fast scrolling; viewability needs 50% for a second
An Arabic ad despite data-lang="en"The fallback rule: the campaign has no English image
The popup never opensThe frequency cap: clear qiads_popup_<placement> or use cap: "always" while testing; and check its image loads
Content jumps when the ad appearsReserving space
A strict CSPCSP, Trusted Types and SRI
16

CSP, Trusted Types and SRI

  • Add https://qi-ads.com to script-src, connect-src and img-src.
  • Styles are applied through adoptedStyleSheets and the CSSOM, so style-src 'unsafe-inline' is not needed in current engines (Safari 16.4, Chrome 73, Firefox 101 and later). Older ones get a <style> element inside the Shadow DOM.
  • No innerHTML, no eval, no string parsed as HTML: the SDK works under require-trusted-types-for 'script'.
  • To pin with a hash: Pinned with Subresource Integrity.
17

Browser and WebView support

Android System WebView and Chrome 61+, iOS and iPadOS (Safari and WKWebView) 12.2+, and current desktop browsers. What an old engine lacks has a fallback: IntersectionObserver → a 100 ms check; CSS aspect-ratio (before iOS 15 and WebView 88) → padding-top; Shadow DOM → scoped styles; sendBeacon → fetch with keepalive. In an engine without Promise the slots collapse (or keep their data-fallback) and the SDK does nothing else.

18

REST API

Every endpoint is public. serve, track and asset are CORS-open and answer OPTIONS; the click link is opened as a browser navigation, never with fetch.

GET /api/ads/serve

ParameterMeaning
placementThe placement id (required)
limit1 to 10, default 7
langar (default), en or ku
siteThe channel id (does not change the answer)
svThe SDK version (does not change the answer)

A 200 answer: {ok, lang, placement:{id, kind, name, aspect_ratio, width, height}, creatives:[{id, booking_id, lang, image, click_url, click_href, alt, title, body, width, height}], ttl, measure:{request, render}}. click_href is the signed click redirect (or null for a creative without a link), booking_id is deprecated and stays until 2.0, and measure says which two counters the client sends. Errors: 400 invalid_placement, 404 unknown_placement, 429 rate_limited (with Retry-After: 60) and 503 unavailable.

GET /api/ads/click/{token}

Redirects (302) to the advertiser's link and counts the click. The link is click_href as served, with optional parameters the SDK adds: s (site), pv, i (impression id), sv, t=1 (test) and wd=1 (navigator.webdriver, filed as automation). The destination always comes from the database, never from the link. A token is valid for 48 hours; an older one is redirected but not counted. The redirect works while the booking is live and for a day after it ended; never for a cancelled or rejected booking (a neutral 410 page). HEAD answers like GET and counts nothing. A speculative load (Sec-Purpose or Purpose: prefetch or prerender) is not redirected: it gets an empty 503, which the browser discards, so the user's real click reaches the server and is counted. Only a load the browser reports as a navigation to a document (Sec-Fetch-Mode: navigate, Sec-Fetch-Dest: document, when it sends them) counts: the link loaded as an image, script, fetch or inside a frame is redirected but not counted (not_navigation). Headers: Cache-Control: no-store, Referrer-Policy: strict-origin-when-cross-origin and X-QiAds-Result.

POST /api/ads/track — version 2 (from 1.3.0)

JSON
{
  "v": 2, "sv": "1.3.0", "site": "superqi", "page": "https://example.com/home", "ref": "https://example.com",
  "pv": "k3j9x0abq2m4z8c1", "now": 1791500000000,
  "events": [
    { "e": "request",  "p": "superqi_homepage_slot", "n": 3, "ts": 1791499999200 },
    { "e": "render",   "p": "superqi_homepage_slot", "m": "<material uuid>", "i": "a1b2c3d4e5f6", "w": 360, "h": 160, "mb": 1, "ts": 1791499999500 },
    { "e": "viewable", "p": "superqi_homepage_slot", "m": "<material uuid>", "i": "a1b2c3d4e5f6", "w": 360, "h": 160, "ts": 1791499999900 }
  ]
}

e is one of request, render, viewable and click (the last only for a creative without click_href). Up to 20 events and 8 KB per beacon; the SDK splits batches at 3.5 KB. test:1 for testing, wd:1 when navigator.webdriver is true. An event more than 15 minutes older than now is not counted. The JSON body is sent as text/plain (no preflight). The answer is 204 with X-QiAds-Result; 400 for an invalid request, 413 for a body over the limit, 429 when rate-limited, 503 when the database is unavailable.

POST /api/ads/track — version 1 (up to 1.2.1, accepted forever)

{v:1, event:"impression"|"click", material_id, booking_id, placement_id, site, page, ref, pv, ts}, up to 4 KB. event: "impression" means a viewable impression. page and ref are cleaned as in version 2.

GET /api/ads/asset/{material_id}

The creative's image, served only while the booking is live, otherwise 404 (kept in the edge cache for a minute). ?lang=en or ?lang=ku for that language's image, the Arabic one when none was uploaded; Content-Language names the one sent.

19

Versioning policy

  • SemVer: only a major release (2.0) removes or changes an attribute, event, method or field; a minor release (1.4) adds; a patch (1.3.1) fixes without changing the interface.
  • /qiads.js follows the latest 1.x. Every pinned release /sdk/<version>/qiads.js stays published.
  • The server accepts version 1 beacons forever, and serve fields are only ever added; a deprecated field stays until the next major release, and the deprecation is announced in the changelog at least one minor release before.
  • Every change is recorded in the changelog.