This is the abridged developer documentation for Qartvelo Ads
# Qartvelo Ads for developers
> Direct-sold banner, interstitial and rewarded ads for Georgian Android apps. Qartvelo Ads campaigns are served first; when there is no ad, the SDK shows your own AdMob unit for the same slot.
## Pick your platform [Section titled “Pick your platform”](#pick-your-platform) [Android (Kotlin)](/android/installation/)com.qartvelo.ads:core 0.3.0 from JitPack. Banner view, interstitial and rewarded APIs. [React Native](/react-native/installation/)@qartvelo/react-native-ads: a TurboModule and Fabric banner on top of the Kotlin SDK. [REST API](/api/overview/)The four endpoints the SDKs use, with an OpenAPI spec, for custom integrations and debugging. [Advertisers](/advertisers/campaigns/)Campaign setup, creative specifications, contextual targeting and prepaid billing. ## Why Qartvelo Ads [Section titled “Why Qartvelo Ads”](#why-qartvelo-ads) Nothing is lost on no-fill Every placement can fall back to **your own** AdMob ad unit. AdMob is preloaded in parallel, so users never wait for the switch. Fast by design An 800 ms request budget by default, a cached remote config for offline starts, and loads that never block on a slow backend. Privacy first Contextual targeting only. No advertising ID, no location, no persistent user identifier. Sessions are random and last an hour. Remote control Kill switches, timeouts, fallback units and banner refresh are changed in the dashboard and reach running apps without an update. ## Made for AI-assisted development [Section titled “Made for AI-assisted development”](#made-for-ai-assisted-development) Every page is available as Markdown, the whole site as [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt), and there is an [MCP server](/ai/mcp-server/) and an [agent skill](/ai/agent-skill/) so Claude Code, Cursor, Copilot and other agents can integrate the SDK correctly on the first try. [llms.txt](/ai/llms-txt/)Machine-readable docs index and full-text bundles. [MCP server](/ai/mcp-server/)Search the docs, generate integration code and send test ad requests from your agent.
# How it works
> The life of an ad request, from SDK start-up to impression, fallback and billing.
This page explains what the SDK does on your behalf. You do not need to implement any of it, but knowing it helps you place ads well and read your reports. ## Start-up [Section titled “Start-up”](#start-up) 1. Your app calls `QartveloAds.initialize(context, appKey, options)` once, usually in `Application.onCreate`. 2. The SDK calls `POST /api/v1/sdk/initialize` with the app key and your package name. The backend checks that the key belongs to an app registered with that exact package name, and returns: * a **session token**, valid for one hour, that carries a random session id (never a user or device id); * the **remote configuration**: kill switches, per-placement request timeout, fallback provider, your AdMob unit id, frequency cap and banner refresh interval. 3. The SDK caches the configuration on disk. On the next start, loads begin immediately from the cached copy while a fresh one is fetched, so a slow or unreachable backend never delays your UI. ## Loading a full-screen ad [Section titled “Loading a full-screen ad”](#loading-a-full-screen-ad)
```plaintext
loadInterstitial("game_end")
├─ Qartvelo Ads request (bounded by the placement timeout, default 800 ms)
│ fill ──> download the creative ──> onLoaded(source = QARTVELO)
│ no fill / timeout / error ──> onFallbackStarted(reason) ──> onLoaded(source = ADMOB)
└─ AdMob preload in parallel (only when a fallback is possible)
```
* **Fill**: the backend selected a campaign and returned a creative URL plus a signed impression token. The SDK downloads the image or video first; an ad is only reported as loaded once it can be displayed instantly. * **No fill**: no eligible campaign, the placement or app is switched off, the frequency cap was reached, or test mode forced it. The response says which fallback to use (`admob` or `none`). * **Timeout**: the backend did not answer within the request timeout. AdMob, already preloading, takes over. * **Slow creative**: if the Qartvelo Ads creative is still downloading 1.5 s after the timeout, a ready AdMob ad completes the load. The Qartvelo Ads ad keeps downloading and is used by the next show. Concurrent loads of the same placement share one request. A Qartvelo Ads ad is valid for 30 minutes; the SDK never shows an expired ad. ## Showing [Section titled “Showing”](#showing) `show*` displays the best ready ad: a valid Qartvelo Ads ad first, otherwise a ready AdMob ad, otherwise `onNoAdAvailable`. When the creative is on screen the SDK reports the **impression** to the backend; taps report a **click** (at most one per ad) before the browser opens. Rewarded ads report the completion once the video finished. Events are sent on a background queue, in order (impression before click), and are retried on network errors. Each impression token can be used exactly once, so replays and duplicates are rejected by the server and never billed. ## Banners [Section titled “Banners”](#banners) A banner placement has one controller per placement code. The view requests an ad, shows it, and refreshes no faster than the placement’s `banner_refresh_seconds` (at least 30 s), only while the view is visible and the Activity is started. If Qartvelo Ads cannot fill, your AdMob adaptive banner is shown in the same view; a later Qartvelo Ads fill replaces it. ## Selection and billing on the server [Section titled “Selection and billing on the server”](#selection-and-billing-on-the-server) For each request the backend picks the highest CPM bid among campaigns that are active, within their schedule and budget, approved, and match the request’s context (country, language, Android version, app, app category and format). Campaign and placement frequency caps use the session id. The chosen campaign’s cost for one impression is reserved atomically, so budgets can never be overspent, and is charged when the impression event arrives. Publishers earn their revenue share of every billed impression. Clicks are recorded but not billed (all campaigns are CPM). ## Remote control [Section titled “Remote control”](#remote-control) Everything below can change without an app update: | Setting | Where | Effect in the app | | ------------------------------------ | ------------------- | -------------------------------------------------------------------- | | Qartvelo Ads on/off per placement | Publisher dashboard | SDK goes straight to the fallback; re-checks at most every 5 minutes | | Fallback provider and AdMob unit | Publisher dashboard | Next load uses the new unit | | Request timeout | Publisher dashboard | Next load | | Banner refresh interval | Publisher dashboard | Next refresh | | App, publisher or global kill switch | Qartvelo Ads admins | All placements fall back |
# Introduction
> What Qartvelo Ads is, who it is for, and how the pieces fit together.
Qartvelo Ads is a direct-sold ad network for Android apps in Georgia. Local advertisers buy CPM campaigns that run inside publishers’ apps as banners, interstitials and rewarded videos. Publishers integrate one SDK and earn a revenue share on every Qartvelo Ads impression, while unfilled requests go to their **own** AdMob account so no inventory is wasted.
```plaintext
Your app ──> Qartvelo Ads SDK ──> Qartvelo Ads ad available?
yes │ │ no / timeout / error
Qartvelo Ads ad AdMob adapter ──> your AdMob ad unit
```
## Who this documentation is for [Section titled “Who this documentation is for”](#who-this-documentation-is-for) | You are | Start here | | --------------------------------------------- | ---------------------------------------------------------------------------------- | | An Android developer (Kotlin or Java) | [Quickstart](/get-started/quickstart/), then [Android SDK](/android/installation/) | | A React Native developer | [React Native installation](/react-native/installation/) | | Building your own client or debugging traffic | [REST API](/api/overview/) and the [OpenAPI spec](/openapi.yaml) | | An advertiser or agency | [Campaigns](/advertisers/campaigns/) and [creative specs](/advertisers/creatives/) | | Using an AI coding assistant | [Build with AI](/ai/overview/) | ## Packages [Section titled “Packages”](#packages) | Package | Install | Notes | | --------------------- | ---------------------------------------- | ------------------------------------------------------------------ | | Android core | `com.qartvelo.ads:core:0.3.0` | JitPack. API client, caching, rendering, events, banner view | | Android AdMob adapter | `com.qartvelo.ads:admob:0.3.0` | Optional. Depends on Google’s `play-services-ads` | | React Native | `npm install @qartvelo/react-native-ads` | Android only for now; iOS calls reject with `unsupported_platform` | The SDK source is MIT-licensed at [github.com/Qartvelo-com/ads](https://github.com/Qartvelo-com/ads). The production API is `https://ads.qartvelo.com/`, which is also the SDK’s default base URL. ## Key concepts [Section titled “Key concepts”](#key-concepts) * **App**: an Android application registered in the publisher dashboard by its package name. Each app gets a public **app key** (`app_` + 24 characters) that you put in your code. * **Placement**: an ad slot in your app, identified by a short **code** such as `home_banner`, `game_end` or `reward_coins`. A placement has one format: `banner`, `interstitial` or `rewarded`. Your code only ever uses the placement code. * **Fallback**: when Qartvelo Ads cannot serve a placement (no eligible campaign, timeout, error, kill switch), the SDK shows your AdMob ad unit for that placement instead. * **Test mode**: a switch in the SDK options that serves built-in test creatives that are never billed, and makes AdMob use Google’s public test units. * **Session**: a short-lived signed token the backend issues at start-up. It carries a random id, never a user or device identifier. ## Platform support [Section titled “Platform support”](#platform-support) | Platform | Status | | ---------------------------------------------- | ----------------------------------------------------------------------------------------- | | Android, native (Kotlin/Java) | Supported, minSdk 23 | | Android, React Native 0.79+ (New Architecture) | Supported, minSdk 24 | | iOS | Not yet. The React Native API is platform-neutral so iOS can be added without API changes | ## Next steps [Section titled “Next steps”](#next-steps) * Follow the [Quickstart](/get-started/quickstart/) to show a test ad in about ten minutes. * Read [How it works](/get-started/how-it-works/) to understand loading, fallback and events.
# Quickstart
> Show your first Qartvelo Ads test ad in an Android or React Native app in about ten minutes.
1. **Create a publisher account and register your app.** Sign up at [ads.qartvelo.com](https://ads.qartvelo.com/register) as a publisher and verify your email. Under **Apps**, add your app with its exact Android package name (`applicationId`). Copy the **app key** shown on the app page (`app_...`). See [Account and apps](/publishers/account-and-apps/). 2. **Create placements.** On the app page, add one placement per ad slot, for example: | Name | Code | Format | | -------------- | -------------- | ------------ | | Home banner | `home_banner` | banner | | Level complete | `game_end` | interstitial | | Free coins | `reward_coins` | rewarded | If you use AdMob, choose **AdMob** as fallback and paste your AdMob ad unit id. See [Placements](/publishers/placements/). 3. **Add the SDK.** * Android (Kotlin) settings.gradle.kts
```kotlin
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}
```
app/build.gradle.kts
```kotlin
dependencies {
implementation("com.qartvelo.ads:core:0.3.0")
implementation("com.qartvelo.ads:admob:0.3.0") // optional AdMob fallback
}
```
* React Native
```sh
npm install @qartvelo/react-native-ads
```
android/build.gradle
```groovy
allprojects {
repositories {
maven {
url "https://jitpack.io"
content { includeGroup("com.qartvelo.ads") }
}
}
}
```
android/gradle.properties
```properties
# Optional: enable the AdMob fallback adapter
QartveloAds_admobEnabled=true
```
If you enabled the AdMob adapter, add **your** AdMob App ID to `AndroidManifest.xml`, or Google’s SDK crashes the app at start-up: AndroidManifest.xml
```xml
```
4. **Initialize in test mode.** * Android (Kotlin) MyApp.kt
```kotlin
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
QartveloAds.initialize(
this,
"app_xxxxxxxxxxxxxxxxxxxxxxxx",
QartveloAdsOptions(testMode = BuildConfig.DEBUG),
)
}
}
```
Register `MyApp` with `android:name=".MyApp"` on the `` element. `BuildConfig.DEBUG` needs `buildFeatures { buildConfig = true }` in `app/build.gradle.kts`. * React Native App.tsx
```tsx
import { useEffect } from 'react';
import { QartveloAds } from '@qartvelo/react-native-ads';
useEffect(() => {
QartveloAds.initialize({
appKey: 'app_xxxxxxxxxxxxxxxxxxxxxxxx',
testMode: __DEV__,
}).catch(() => {
// The SDK keeps working on its cached config and the AdMob fallback.
});
}, []);
```
5. **Show an ad.** * Android (Kotlin)
```kotlin
// Preload early, for example when the level starts
QartveloAds.loadInterstitial("game_end")
// Show at a natural break
QartveloAds.showInterstitial(this, "game_end", object : QartveloAdsListener {
override fun onDismissed(info: QartveloAdsAdInfo) = continueGame()
override fun onNoAdAvailable(placementId: String, format: AdFormat) = continueGame()
})
```
activity_main.xml
```xml
```
```kotlin
findViewById(R.id.banner).load()
```
* React Native
```tsx
import { QartveloAds, QartveloAdsBanner } from '@qartvelo/react-native-ads';
await QartveloAds.loadInterstitial('game_end');
const { shown } = await QartveloAds.showInterstitial('game_end'); // resolves on dismiss
;
```
Rebuild the native app after installing: `npx react-native run-android`. You should see a purple creative labelled **TEST AD**. Set `testForceNoFill` to `true` as well to see your AdMob fallback (Google’s test ad) instead. 6. **Go live.** Wait for the app to be approved, turn test mode off for release builds and work through the [release checklist](/android/release-checklist/). ## Use an AI assistant instead [Section titled “Use an AI assistant instead”](#use-an-ai-assistant-instead) Paste this into Claude Code, Cursor or Copilot Chat in your project:
```text
Integrate the Qartvelo Ads Android SDK into this app following https://developers.qartvelo.com/llms-full.txt.
App key: app_xxxxxxxxxxxxxxxxxxxxxxxx. Placements: home_banner (banner), game_end (interstitial),
reward_coins (rewarded). Use test mode in debug builds and keep my existing AdMob units as fallback.
```
More prompts and the MCP server are in [Build with AI](/ai/overview/).
# Test mode
> Develop and QA with non-billable test ads, and force a no-fill to see your AdMob fallback.
Never click or watch live ads in your own app: it generates invalid traffic, which is rejected and can get your app suspended. Use test mode for every debug and QA build. ## Turn it on [Section titled “Turn it on”](#turn-it-on) | Platform | Option | | ------------ | ----------------------------------------------------------- | | Android | `QartveloAdsOptions(testMode = BuildConfig.DEBUG)` | | React Native | `QartveloAds.initialize({ appKey, testMode: __DEV__ })` | | REST API | `"test_mode": true` on `/sdk/initialize` and `/ads/request` | Test mode is read only from your code; it is never cached, so turning it off takes effect on the next app start. ## What changes [Section titled “What changes”](#what-changes) * The backend never selects live campaigns and never reserves budget. It returns built-in creatives labelled **TEST AD**: | Format | Creative | | ------------ | ------------------ | | banner | 320x50 PNG | | interstitial | 1080x1920 PNG | | rewarded | 15 s, 720x1280 MP4 | * Impressions, clicks and rewards are validated and de-duplicated exactly like live ones, so you can test your event handling, but they are never billed, never earn revenue and never appear in reports. * The app does not need to be approved yet: test sessions skip the approval check. * The AdMob adapter replaces your unit ids with Google’s public test units: | Format | Google test unit | | ------------ | ---------------------------------------- | | banner | `ca-app-pub-3940256099942544/9214589741` | | interstitial | `ca-app-pub-3940256099942544/1033173712` | | rewarded | `ca-app-pub-3940256099942544/5224354917` | ## Exercise the fallback [Section titled “Exercise the fallback”](#exercise-the-fallback) Add `testForceNoFill = true` (`testForceNoFill: true` in React Native). Every Qartvelo Ads request answers `no_fill` with reason `test_no_fill`, so each load emits `onFallbackStarted` and then shows a Google test ad. Google’s rewarded test unit grants 10 coins. ## Logs [Section titled “Logs”](#logs) Set `logLevel = QartveloAdsLogLevel.DEBUG` (`logLevel: 'debug'`) and filter logcat by the tag `QartveloAds`:
```sh
adb logcat -s QartveloAds
```
Debug logs include request timing, fallback decisions and event delivery. Tokens are never logged. ## Sample apps [Section titled “Sample apps”](#sample-apps) The [SDK repository](https://github.com/Qartvelo-com/ads) contains a native sample (`android/sample-app`) and a React Native example (`react-native/example`). Both have Load/Show buttons, a banner screen, test-mode and force-no-fill switches, an editable base URL and an on-screen event log. Caution Before you publish, make sure release builds use `testMode = false` and `testForceNoFill = false`. Test traffic earns nothing.
# API reference
> Complete public API of the Qartvelo Ads Android SDK 0.3.0 (package com.qartvelo.sdk).
Package `com.qartvelo.sdk`, version `0.3.0` (`QartveloAds.SDK_VERSION`). Every method is safe to call from any thread, never throws and delivers callbacks on the main thread. `@JvmStatic` and `@JvmOverloads` make the API callable from Java as `QartveloAds.initialize(...)`. ## QartveloAds [Section titled “QartveloAds”](#qartveloads)
```kotlin
object QartveloAds {
const val SDK_VERSION: String = "0.3.0"
fun initialize(
context: Context,
appKey: String,
options: QartveloAdsOptions = QartveloAdsOptions(),
listener: QartveloAdsInitListener? = null,
)
fun isInitialized(): Boolean
fun loadInterstitial(placementId: String, listener: QartveloAdsListener? = null)
fun showInterstitial(activity: Activity, placementId: String, listener: QartveloAdsListener? = null)
fun isInterstitialReady(placementId: String): Boolean
fun loadRewarded(placementId: String, listener: QartveloAdsListener? = null)
fun showRewarded(activity: Activity, placementId: String, listener: QartveloAdsListener? = null)
fun isRewardedReady(placementId: String): Boolean
fun addEventListener(listener: QartveloAdsListener)
fun removeEventListener(listener: QartveloAdsListener)
fun setLogLevel(level: QartveloAdsLogLevel)
fun setPrivacy(privacy: QartveloAdsPrivacy)
fun registerFallbackAdapter(adapter: FallbackAdapter)
}
```
| Method | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `initialize` | Starts the SDK once per process. See [Initialization](/android/initialization/) | | `isInitialized` | `true` once the first initialization attempt finished (also offline) | | `loadInterstitial` / `loadRewarded` | Loads an ad for a placement code; ends with `onLoaded` or `onLoadFailed` | | `showInterstitial` / `showRewarded` | Shows the best ready ad; ends with `onDismissed`, `onNoAdAvailable` or `onLoadFailed` | | `isInterstitialReady` / `isRewardedReady` | Whether a show would display an ad now | | `addEventListener` / `removeEventListener` | Global observers for every placement, banners included | | `setLogLevel` | Overrides `options.logLevel` at runtime | | `setPrivacy` | Privacy signals forwarded to the fallback adapter. See [Privacy](/guides/privacy/) | | `registerFallbackAdapter` | Use a custom fallback network. See [Custom fallback adapter](/guides/custom-fallback-adapter/) | ## QartveloAdsOptions [Section titled “QartveloAdsOptions”](#qartveloadsoptions)
```kotlin
data class QartveloAdsOptions(
val admobFallback: Boolean = true,
val requestTimeoutMs: Long = 800,
val testMode: Boolean = false,
val testForceNoFill: Boolean = false,
val logLevel: QartveloAdsLogLevel = QartveloAdsLogLevel.ERROR,
val baseUrl: String = DEFAULT_BASE_URL, // "https://ads.qartvelo.com/"
val admobAdUnits: Map = emptyMap(),
)
```
## QartveloAdsBannerView [Section titled “QartveloAdsBannerView”](#qartveloadsbannerview)
```kotlin
class QartveloAdsBannerView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout {
var placementId: String? // XML: app:qartvelo_placementId
var listener: QartveloAdsListener?
fun load() // idempotent
fun destroy() // detaches; the loaded banner stays cached
}
```
## Listeners [Section titled “Listeners”](#listeners)
```kotlin
interface QartveloAdsListener {
fun onLoaded(info: QartveloAdsAdInfo) {}
fun onLoadFailed(placementId: String, error: QartveloAdsError) {}
fun onShown(info: QartveloAdsAdInfo) {}
fun onImpression(info: QartveloAdsAdInfo) {}
fun onClicked(info: QartveloAdsAdInfo) {}
fun onDismissed(info: QartveloAdsAdInfo) {}
fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) {}
fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) {}
fun onNoAdAvailable(placementId: String, format: AdFormat) {}
}
fun interface QartveloAdsInitListener {
fun onInitialized(success: Boolean, error: QartveloAdsError?)
}
```
## Models [Section titled “Models”](#models)
```kotlin
enum class QartveloAdsLogLevel { NONE, ERROR, INFO, DEBUG }
enum class AdFormat { BANNER, INTERSTITIAL, REWARDED }
enum class AdSource { QARTVELO, ADMOB }
data class QartveloAdsAdInfo(
val placementId: String,
val format: AdFormat,
val source: AdSource,
val campaignId: String? = null,
val creativeId: String? = null,
)
data class QartveloAdsReward(val type: String = "reward", val amount: Int = 1)
enum class QartveloAdsErrorCode {
NOT_INITIALIZED, INVALID_PLACEMENT, NETWORK_ERROR, TIMEOUT, NO_FILL,
CREATIVE_FAILED, AD_EXPIRED, SHOW_FAILED, ALREADY_SHOWING, INTERNAL_ERROR,
}
data class QartveloAdsError(val code: QartveloAdsErrorCode, val message: String)
data class QartveloAdsPrivacy(
val consentGiven: Boolean? = null, // null = unknown
val childDirected: Boolean? = null,
val underAgeOfConsent: Boolean? = null,
)
```
## Fallback seam [Section titled “Fallback seam”](#fallback-seam) Package `com.qartvelo.sdk.fallback`: `FallbackAdapter`, `FallbackSettings`, `FallbackLoadCallback`, `FallbackShowCallback`, `FallbackBannerCallback`, `FallbackBanner`. Documented in [Custom fallback adapter](/guides/custom-fallback-adapter/). ## Java [Section titled “Java”](#java)
```java
QartveloAds.initialize(this, "app_xxxxxxxxxxxxxxxxxxxxxxxx",
new QartveloAdsOptions(true, 800L, BuildConfig.DEBUG, false,
QartveloAdsLogLevel.ERROR, "https://ads.qartvelo.com/", Collections.emptyMap()));
QartveloAds.loadInterstitial("game_end");
QartveloAds.showInterstitial(activity, "game_end");
```
`QartveloAdsOptions` has `@JvmOverloads`, so leading arguments can be passed and the rest default (for example `new QartveloAdsOptions(true, 800L, BuildConfig.DEBUG)`).
# Banner
> Add a QartveloAdsBannerView in XML or code, listen to its events and manage its lifecycle.
## In XML [Section titled “In XML”](#in-xml) res/layout/activity_main.xml
```xml
```
MainActivity.kt
```kotlin
class MainActivity : AppCompatActivity() {
private lateinit var banner: QartveloAdsBannerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
banner = findViewById(R.id.banner)
banner.listener = object : QartveloAdsListener {
override fun onLoaded(info: QartveloAdsAdInfo) {
// info.source is AdSource.QARTVELO or AdSource.ADMOB
}
override fun onNoAdAvailable(placementId: String, format: AdFormat) {
banner.visibility = View.GONE
}
}
banner.load() // idempotent
}
override fun onDestroy() {
banner.destroy() // the loaded banner stays cached for the next view
super.onDestroy()
}
}
```
## In code [Section titled “In code”](#in-code)
```kotlin
val banner = QartveloAdsBannerView(this).apply {
placementId = "home_banner"
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
container.addView(banner)
banner.load()
```
## In Jetpack Compose [Section titled “In Jetpack Compose”](#in-jetpack-compose)
```kotlin
@Composable
fun QartveloBanner(placementId: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier.fillMaxWidth(),
factory = { context ->
QartveloAdsBannerView(context).apply {
this.placementId = placementId
load()
}
},
onRelease = { it.destroy() },
)
}
```
## Behaviour [Section titled “Behaviour”](#behaviour) * **One controller per placement code.** A re-created view (rotation, list recycling, Compose recomposition, React Native re-render) that calls `load()` again re-attaches to the loaded banner; no new request is made. * **Refresh** happens no faster than the placement’s `banner_refresh_seconds` (at least 30 s), and only while the view is attached and visible. It pauses when the Activity stops or the view is hidden or detached. * **Fallback**: on Qartvelo Ads no-fill, timeout, error or creative failure the view renders your AdMob anchored adaptive banner instead. A visible AdMob banner refreshes itself (AdMob settings apply); a later Qartvelo Ads fill replaces it. * **One visible banner per placement code.** Use distinct placement codes for banners that are on screen at the same time. * **No leaks.** The view never holds an Activity after it is detached; AdMob views are re-parented through a context wrapper. * Qartvelo Ads banner creatives keep their aspect ratio within the view width. Supported creative sizes are 320x50, 320x100, 300x250, 468x60 and 728x90. ## Banner events [Section titled “Banner events”](#banner-events) Banners use the same [`QartveloAdsListener`](/android/events/): `onLoaded`, `onLoadFailed`, `onShown`, `onImpression`, `onClicked`, `onFallbackStarted` and `onNoAdAvailable`. `onDismissed` and `onReward` do not apply. ## Errors [Section titled “Errors”](#errors) | Situation | Callback | | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `placementId` not set | `onLoadFailed(INVALID_PLACEMENT)` | | `load()` before `QartveloAds.initialize` | `onLoadFailed(NOT_INITIALIZED)` | | The code belongs to a non-banner placement, or does not exist | `onLoadFailed(INVALID_PLACEMENT)` | | No source has an ad | `onNoAdAvailable`, then `onLoadFailed` with the Qartvelo Ads failure (`NO_FILL`, `TIMEOUT`, `NETWORK_ERROR` or `CREATIVE_FAILED`) | After a failure the banner keeps its refresh schedule and tries again; you do not need to call `load()` again.
# Events and errors
> QartveloAdsListener callbacks, global observers, event ordering and error codes.
All callbacks run on the **main thread**. Every method of `QartveloAdsListener` has an empty default body, so override only what you need.
```kotlin
interface QartveloAdsListener {
fun onLoaded(info: QartveloAdsAdInfo) {}
fun onLoadFailed(placementId: String, error: QartveloAdsError) {}
fun onShown(info: QartveloAdsAdInfo) {}
fun onImpression(info: QartveloAdsAdInfo) {}
fun onClicked(info: QartveloAdsAdInfo) {}
fun onDismissed(info: QartveloAdsAdInfo) {}
fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) {}
fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) {}
fun onNoAdAvailable(placementId: String, format: AdFormat) {}
}
```
## Callbacks [Section titled “Callbacks”](#callbacks) | Callback | When | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | `onLoaded(info)` | A load finished and a source is ready. `info.source` is the one that will be shown | | `onLoadFailed(placementId, error)` | A load ended without an ad, or a show-time error (`ALREADY_SHOWING`, `SHOW_FAILED`) | | `onFallbackStarted(placementId, format, reason)` | Qartvelo Ads could not serve. `reason`: `no_fill`, `timeout`, `error`, `creative_failed`, `disabled` | | `onNoAdAvailable(placementId, format)` | No source can serve. Before `onLoadFailed` on loads; alone on shows | | `onShown(info)` | The creative is on screen | | `onImpression(info)` | The impression was counted (one per ad) | | `onClicked(info)` | First tap on the ad, recorded before the browser opens | | `onReward(info, reward)` | Rewarded completion, at most once per show | | `onDismissed(info)` | The full-screen ad closed | ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) * A **load** ends with exactly one of `onLoaded` or `onLoadFailed`. `onFallbackStarted` may precede either. * A **show** produces `onShown` and `onImpression` when the creative is on screen, `onClicked` at most once, `onReward` at most once (rewarded only), and ends with exactly one of `onDismissed`, `onNoAdAvailable` or `onLoadFailed`. ## Global observers [Section titled “Global observers”](#global-observers) Per-call listeners receive the events of that call. Global observers receive every event of every placement and format, including banners. Use them for analytics:
```kotlin
val analytics = object : QartveloAdsListener {
override fun onImpression(info: QartveloAdsAdInfo) {
log("ad_impression", info.placementId, info.source.name)
}
override fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) {
log("ad_fallback", placementId, reason)
}
}
QartveloAds.addEventListener(analytics)
// later
QartveloAds.removeEventListener(analytics)
```
## Ad info [Section titled “Ad info”](#ad-info)
```kotlin
data class QartveloAdsAdInfo(
val placementId: String,
val format: AdFormat, // BANNER, INTERSTITIAL, REWARDED
val source: AdSource, // QARTVELO or ADMOB
val campaignId: String? = null, // "cmp_12" for Qartvelo Ads ads, null for AdMob
val creativeId: String? = null, // "cr_34" for Qartvelo Ads ads, null for AdMob
)
```
## Error codes [Section titled “Error codes”](#error-codes) `QartveloAdsError(code: QartveloAdsErrorCode, message: String)` | Code | Meaning | | ------------------- | ------------------------------------------------------------------------------------------ | | `NOT_INITIALIZED` | `initialize` was not called, the app key is empty, or the backend rejected the key/package | | `INVALID_PLACEMENT` | Empty or unknown placement code, or a placement used with the wrong format | | `NETWORK_ERROR` | The backend could not be reached and no fallback was available | | `TIMEOUT` | The backend did not answer in time and no fallback was available | | `NO_FILL` | Neither Qartvelo Ads nor the fallback had an ad | | `CREATIVE_FAILED` | The creative could not be downloaded or decoded | | `AD_EXPIRED` | The ad passed its expiry before it was shown | | `SHOW_FAILED` | The ad could not be displayed (for example the Activity was finishing) | | `ALREADY_SHOWING` | Another full-screen ad is on screen | | `INTERNAL_ERROR` | Unexpected failure inside the SDK. The SDK never throws into your code | The `message` is for logs only; branch on `code`.
# Initialization
> Initialize the SDK once, configure options, and pass privacy signals.
Initialize once, as early as possible, normally in `Application.onCreate`. The call returns immediately; networking happens in the background. MyApp.kt
```kotlin
import com.qartvelo.sdk.QartveloAds
import com.qartvelo.sdk.QartveloAdsLogLevel
import com.qartvelo.sdk.QartveloAdsOptions
import com.qartvelo.sdk.QartveloAdsPrivacy
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Optional, any time: privacy signals from your consent flow (see Privacy guide)
QartveloAds.setPrivacy(QartveloAdsPrivacy(consentGiven = consentOrNull()))
QartveloAds.initialize(
context = this,
appKey = "app_xxxxxxxxxxxxxxxxxxxxxxxx",
options = QartveloAdsOptions(
testMode = BuildConfig.DEBUG,
logLevel = if (BuildConfig.DEBUG) QartveloAdsLogLevel.DEBUG else QartveloAdsLogLevel.ERROR,
),
) { success, error ->
// Main thread. success == false still leaves the SDK usable.
}
}
}
```
## Options [Section titled “Options”](#options) `QartveloAdsOptions` is a data class; every field has a default. | Option | Type | Default | Meaning | | ------------------ | --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `admobFallback` | `Boolean` | `true` | Allow the AdMob adapter to serve your units when Qartvelo Ads cannot | | `requestTimeoutMs` | `Long` | `800` | Qartvelo Ads time budget before falling back. A per-placement value from the dashboard wins. Clamped to 100..10000 | | `testMode` | `Boolean` | `false` | Non-billable test ads; AdMob uses Google’s test units. See [Test mode](/get-started/test-mode/) | | `testForceNoFill` | `Boolean` | `false` | Force Qartvelo Ads `no_fill` to exercise the fallback | | `logLevel` | `QartveloAdsLogLevel` | `ERROR` | `NONE`, `ERROR`, `INFO`, `DEBUG`. Logcat tag `QartveloAds` | | `baseUrl` | `String` | `https://ads.qartvelo.com/` | API origin. Change only for a self-hosted or local backend | | `admobAdUnits` | `Map` | empty | Placement code to your AdMob ad unit id. Wins over the dashboard value | ## Behaviour [Section titled “Behaviour”](#behaviour) * **Idempotent.** Only the first call counts. Later calls are ignored (a different app key or options are logged at `INFO`), but their listener still receives the first result. Restart the process to change options. * **Only the application context is kept.** Passing an Activity is safe. * **Never blocks loads on a slow backend.** With a cached remote config, loads start immediately and the Qartvelo Ads request obtains its session within its own timeout. Only the very first start (no cache yet) holds loads for up to `requestTimeoutMs` while the first configuration arrives. * **Failure is not fatal.** If initialization fails (offline, backend down, key rejected), the listener gets `success = false`, the SDK runs on its cached config and can still fall back to AdMob. The session is retried lazily and throttled, so an outage never blocks your UI. * `QartveloAds.isInitialized()` becomes `true` when the first attempt has finished, successfully or not. * Loads before `initialize` was called at all fail with `NOT_INITIALIZED`. ## Initialization listener [Section titled “Initialization listener”](#initialization-listener)
```kotlin
fun interface QartveloAdsInitListener {
fun onInitialized(success: Boolean, error: QartveloAdsError?)
}
```
Typical errors: `NETWORK_ERROR` and `TIMEOUT` (backend unreachable), `NOT_INITIALIZED` (empty or rejected app key, wrong package name, app not approved outside test mode). ## Runtime settings [Section titled “Runtime settings”](#runtime-settings)
```kotlin
QartveloAds.setLogLevel(QartveloAdsLogLevel.DEBUG) // overrides options.logLevel
QartveloAds.setPrivacy(QartveloAdsPrivacy(childDirected = true))
```
Both can be called before or after `initialize`. See the [Privacy guide](/guides/privacy/) for what the privacy signals do. ## App key safety [Section titled “App key safety”](#app-key-safety) The app key is public and identifies your app; the backend accepts it only together with the package name registered in the dashboard. Do not put the SDK secret in your app.
# Installation
> Add the Qartvelo Ads Android SDK and the optional AdMob adapter to your Gradle build.
| Artifact | Contents | Required | | ------------------------------ | ------------------------------------------------------------------- | -------- | | `com.qartvelo.ads:core:0.3.0` | API client, config cache, rendering, event delivery, banner view | Yes | | `com.qartvelo.ads:admob:0.3.0` | Fallback adapter for Google Mobile Ads (`play-services-ads` 25.4.0) | Optional | ## Requirements [Section titled “Requirements”](#requirements) * `minSdk` 23 or higher, `compileSdk` 35 or higher * Java 17 toolchain, AndroidX * Core brings OkHttp 4.12, Media3 ExoPlayer 1.8 and AndroidX core. It does not depend on Google Mobile Ads; only the adapter does, as a normal (never shaded) dependency, so a newer `play-services-ads` in your app wins. ## Add the repository and dependencies [Section titled “Add the repository and dependencies”](#add-the-repository-and-dependencies) The SDK is published on JitPack (built from the [Qartvelo-com/ads](https://github.com/Qartvelo-com/ads) tags). No account or token is needed. * Kotlin DSL settings.gradle.kts
```kotlin
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io") {
content { includeGroup("com.qartvelo.ads") }
}
}
}
```
app/build.gradle.kts
```kotlin
dependencies {
implementation("com.qartvelo.ads:core:0.3.0")
// Optional: fall back to your own AdMob ad units. Leave out to run Qartvelo Ads only.
implementation("com.qartvelo.ads:admob:0.3.0")
}
```
* Groovy settings.gradle
```groovy
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url "https://jitpack.io"
content { includeGroup("com.qartvelo.ads") }
}
}
}
```
app/build.gradle
```groovy
dependencies {
implementation "com.qartvelo.ads:core:0.3.0"
implementation "com.qartvelo.ads:admob:0.3.0" // optional
}
```
* Version catalog gradle/libs.versions.toml
```toml
[versions]
qartvelo-ads = "0.3.0"
[libraries]
qartvelo-ads-core = { module = "com.qartvelo.ads:core", version.ref = "qartvelo-ads" }
qartvelo-ads-admob = { module = "com.qartvelo.ads:admob", version.ref = "qartvelo-ads" }
```
app/build.gradle.kts
```kotlin
dependencies {
implementation(libs.qartvelo.ads.core)
implementation(libs.qartvelo.ads.admob)
}
```
The adapter is discovered at runtime by class name, so adding or removing `com.qartvelo.ads:admob` needs no code change. Without it, the SDK works normally and reports `onNoAdAvailable` when Qartvelo Ads has no ad. ## Manifest [Section titled “Manifest”](#manifest) `core` merges the `INTERNET` permission and its full-screen ad Activity automatically. Nothing else is needed for Qartvelo Ads. If you include the AdMob adapter, Google requires **your own** AdMob App ID, or the Google Mobile Ads SDK crashes the app at start-up: app/src/main/AndroidManifest.xml
```xml
```
For development you can use Google’s sample App ID `ca-app-pub-3940256099942544~3347511713`. ## R8 / ProGuard [Section titled “R8 / ProGuard”](#r8--proguard) Both libraries ship consumer rules. Minified release builds need no extra configuration. ## Local backend (optional) [Section titled “Local backend (optional)”](#local-backend-optional) If you run your own Qartvelo Ads backend on your machine for development, the emulator reaches it at `http://10.0.2.2:8000/`. Allow cleartext HTTP for that host in **debug builds only**: app/src/debug/res/xml/network_security_config.xml
```xml
10.0.2.2
localhost
```
app/src/debug/AndroidManifest.xml
```xml
```
Production always uses HTTPS (`https://ads.qartvelo.com/`, the default). Next: [Initialization](/android/initialization/).
# Interstitial
> Preload and show full-screen interstitial ads at natural breaks.
Interstitials are full-screen image or video ads shown at natural pauses: between levels, after finishing an article, when leaving a screen. ## Load early, show later [Section titled “Load early, show later”](#load-early-show-later)
```kotlin
// When the level starts
QartveloAds.loadInterstitial("game_end", object : QartveloAdsListener {
override fun onLoaded(info: QartveloAdsAdInfo) {
// ready; info.source is QARTVELO or ADMOB
}
override fun onLoadFailed(placementId: String, error: QartveloAdsError) {
// nothing ready; error.code tells you why
}
})
// When the level ends
if (QartveloAds.isInterstitialReady("game_end")) {
QartveloAds.showInterstitial(activity, "game_end", object : QartveloAdsListener {
override fun onDismissed(info: QartveloAdsAdInfo) = continueGame()
override fun onNoAdAvailable(placementId: String, format: AdFormat) = continueGame()
override fun onLoadFailed(placementId: String, error: QartveloAdsError) = continueGame()
})
} else {
continueGame()
}
```
Always continue your flow from **every** terminal callback of the show: `onDismissed`, `onNoAdAvailable` and `onLoadFailed`. Exactly one of them ends each show. ## Load behaviour [Section titled “Load behaviour”](#load-behaviour) 1. A still-valid cached Qartvelo Ads ad completes the load immediately. 2. Otherwise the SDK requests Qartvelo Ads and, when a fallback is possible, **preloads AdMob in parallel**. 3. On a fill, the creative is downloaded and validated, then `onLoaded(source = QARTVELO)`. 4. On no-fill, timeout, network error, creative failure or kill switch: `onFallbackStarted(reason)`, then `onLoaded(source = ADMOB)` when AdMob is ready, or `onNoAdAvailable` followed by `onLoadFailed` if AdMob has nothing either. 5. If the Qartvelo Ads creative is still downloading 1.5 s after the request timeout, a ready AdMob ad completes the load; the Qartvelo Ads download continues and is used by the next show. Concurrent loads of the same placement share one request. ## Show behaviour [Section titled “Show behaviour”](#show-behaviour) * Show prefers a valid Qartvelo Ads ad, then a ready AdMob ad, else `onNoAdAvailable`. * If a Qartvelo Ads creative fails to render before anything was displayed, a ready AdMob ad is shown instead. * Only one full-screen ad can be on screen; a second show fails with `onLoadFailed(ALREADY_SHOWING)`. * A Qartvelo Ads ad is shown at most once and never after its expiry (30 minutes after the request). * Rotation and Activity re-creation do not restart the ad or repeat events. * Tapping the ad records the click, then opens the advertiser’s URL in the browser. ## Listeners [Section titled “Listeners”](#listeners) Pass a listener to `showInterstitial`. Without one, show events go to the last load listener of that placement while your code still references it (load listeners are held weakly so a destroyed Activity is never retained), plus any global observers registered with `QartveloAds.addEventListener`. ## Good practice [Section titled “Good practice”](#good-practice) * Preload as soon as you know an ad might be shown, not right before showing. * Do not show interstitials on app launch, in the middle of gameplay, or right after another full-screen ad. * Use a frequency cap on the placement (for example 1 per `hour`) rather than counting in code. * After a show, load again for the next opportunity.
# Release checklist
> Everything to verify before shipping a build with Qartvelo Ads to production.
* [ ] The app and its placements are **approved** in the dashboard and placements are `active`. * [ ] The release `applicationId` equals the package name registered for the app. * [ ] `testMode = false` and `testForceNoFill = false` in release builds (for example `testMode = BuildConfig.DEBUG`). * [ ] `baseUrl` is the default `https://ads.qartvelo.com/` (or your production HTTPS origin); no cleartext network config in the release manifest. * [ ] Your **own** AdMob App ID is in the manifest and your own ad unit ids are set per placement (dashboard or `admobAdUnits`); no Google test ids in production. See [AdMob fallback](/guides/admob-fallback/). * [ ] Consent is collected by your CMP where required and passed with `QartveloAds.setPrivacy`. See [Privacy](/guides/privacy/). * [ ] `logLevel` is `ERROR` or `NONE`. * [ ] A minified (R8) release build was tested once on a real device. Consumer rules are bundled. * [ ] `banner.destroy()` is called when banner screens are destroyed. * [ ] Your game or app flow continues from every terminal show callback (`onDismissed`, `onNoAdAvailable`, `onLoadFailed`). * [ ] Rewards are granted only from `onReward` (or `result.rewarded` in React Native), never from `onDismissed`. * [ ] Your Google Play Data safety form reflects what the SDK sends. See [Privacy](/guides/privacy/#google-play-data-safety). After release, watch the **Fill rate** and **Fallbacks** columns in your reports. Many fallbacks with a healthy fill rate usually mean requests time out: check a debug build’s `onFallbackStarted` reasons and consider raising the placement’s request timeout.
# Rewarded
> Show rewarded videos and grant rewards exactly once, only after confirmed completion.
Rewarded placements play a video the user opts into in exchange for an in-app reward (coins, an extra life, a hint).
```kotlin
// Preload, for example when the shop screen opens
QartveloAds.loadRewarded("reward_coins")
// The user taps "Watch a video for 50 coins"
QartveloAds.showRewarded(activity, "reward_coins", object : QartveloAdsListener {
override fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) {
grantCoins(50) // exactly once, only after completion
}
override fun onDismissed(info: QartveloAdsAdInfo) = resumeGame()
override fun onNoAdAvailable(placementId: String, format: AdFormat) = showNoVideoMessage()
override fun onLoadFailed(placementId: String, error: QartveloAdsError) = showNoVideoMessage()
})
```
Use `QartveloAds.isRewardedReady("reward_coins")` to enable or hide the “watch a video” button. ## Reward guarantees [Section titled “Reward guarantees”](#reward-guarantees) * `onReward` fires **at most once per show** and **only after confirmed completion**, whether Qartvelo Ads or the AdMob fallback served the ad. * Qartvelo Ads rewards are `QartveloAdsReward(type = "reward", amount = 1)`. AdMob rewards carry the type and amount configured on your AdMob unit (`onUserEarnedReward` is mapped to `onReward`). Most apps ignore the amount and grant their own fixed reward. * Closing a Qartvelo Ads video early asks the user for confirmation and forfeits the reward. * Rotation and Activity re-creation neither restart the video nor repeat events. * The SDK reports the completion to the backend (`POST /events/reward`) for auditing; your app should grant the reward from the callback, not wait for the server. ## Video playback [Section titled “Video playback”](#video-playback) Qartvelo Ads rewarded videos are MP4 (H.264), 5 to 60 seconds long, played full-screen with Media3 ExoPlayer from a file downloaded before `onLoaded`. Playback never starts streaming over a slow network.
# API reference
> Complete TypeScript API of @qartvelo/react-native-ads 0.3.0.
```ts
import {
QartveloAds, // also the default export
QartveloAdsBanner,
QartveloAdsError,
isQartveloAdsError,
} from '@qartvelo/react-native-ads';
```
## QartveloAds [Section titled “QartveloAds”](#qartveloads) | Method | Returns | Description | | --------------------------------------------- | ----------------------------- | ---------------------------------------------------------------- | | `initialize(options: QartveloAdsInitOptions)` | `Promise` | Starts the SDK once per process | | `isInitialized()` | `Promise` | First initialization attempt finished | | `loadInterstitial(placementId)` | `Promise` | Resolves when an ad is ready, rejects when none | | `showInterstitial(placementId)` | `Promise` | Resolves on dismiss, or `{ shown: false }` when nothing is ready | | `isInterstitialReady(placementId)` | `Promise` | | | `loadRewarded(placementId)` | `Promise` | | | `showRewarded(placementId)` | `Promise` | `rewarded` is true only after confirmed completion | | `isRewardedReady(placementId)` | `Promise` | | | `addListener(type, listener)` | `QartveloAdsSubscription` | Observe events of all placements | | `removeAllListeners(type?)` | `void` | | | `setLogLevel(level: LogLevel)` | `void` | No-op where unsupported | | `setPrivacy(privacy: QartveloAdsPrivacy)` | `void` | No-op where unsupported | | `isSupported()` | `boolean` | Android with the native module built in | ## QartveloAdsBanner props [Section titled “QartveloAdsBanner props”](#qartveloadsbanner-props) | Prop | Type | | -------------------------------------- | ---------------------------------------------------------------------------------------- | | `placementId` | `string` (required) | | `style` | `StyleProp`; usually `{ width: '100%' }` | | `onLoaded` | `(e: QartveloAdsEventMap['loaded']) => void` | | `onLoadFailed` | `(e: QartveloAdsEventMap['loadFailed']) => void` | | `onShown`, `onImpression`, `onClicked` | `(e) => void` | | `onFallbackStarted` | `(e: QartveloAdsEventMap['fallbackStarted']) => void` | | `onNoAdAvailable` | `(e: QartveloAdsEventMap['noAdAvailable']) => void` | | `onSizeChange` | `(size: { width: number; height: number }) => void` (dp; 0 x 0 when nothing is rendered) | Other `ViewProps` (such as `testID`) are passed through. ## Types [Section titled “Types”](#types)
```ts
type AdFormat = 'banner' | 'interstitial' | 'rewarded';
type AdSource = 'qartvelo' | 'admob';
type LogLevel = 'none' | 'error' | 'info' | 'debug';
type FallbackReason = 'no_fill' | 'timeout' | 'error' | 'creative_failed' | 'disabled';
interface QartveloAdsInitOptions {
appKey: string;
requestTimeoutMs?: number;
testMode?: boolean;
testForceNoFill?: boolean;
admobFallback?: boolean;
logLevel?: LogLevel;
baseUrl?: string;
admobAdUnits?: Record;
}
interface AdInfo {
placementId: string;
format: AdFormat;
source: AdSource;
campaignId?: string; // Qartvelo Ads ads only
creativeId?: string; // Qartvelo Ads ads only
}
interface Reward { type: string; amount: number }
interface ShowResult { shown: boolean; source?: AdSource }
interface RewardedShowResult extends ShowResult { rewarded: boolean; reward?: Reward }
interface QartveloAdsPrivacy {
consentGiven?: boolean;
childDirected?: boolean;
underAgeOfConsent?: boolean;
}
type QartveloAdsErrorCode =
| 'not_initialized' | 'invalid_placement' | 'network_error' | 'timeout' | 'no_fill'
| 'creative_failed' | 'ad_expired' | 'show_failed' | 'already_showing' | 'internal_error'
| 'unsupported_platform' | 'module_unavailable' | 'invalid_argument';
class QartveloAdsError extends Error {
readonly code: QartveloAdsErrorCode;
readonly placementId?: string;
}
interface QartveloAdsSubscription { remove(): void }
```
Event payload types are exported as `QartveloAdsEventMap`, `QartveloAdsEvent`, `QartveloAdsEventType` and `QartveloAdsEventListener`. ## Testing with Jest [Section titled “Testing with Jest”](#testing-with-jest) Mock the package in your own tests:
```ts
jest.mock('@qartvelo/react-native-ads', () => ({
QartveloAds: {
initialize: jest.fn().mockResolvedValue(undefined),
loadInterstitial: jest.fn().mockResolvedValue({ placementId: 'game_end', format: 'interstitial', source: 'qartvelo' }),
showInterstitial: jest.fn().mockResolvedValue({ shown: true, source: 'qartvelo' }),
loadRewarded: jest.fn(),
showRewarded: jest.fn().mockResolvedValue({ shown: true, rewarded: true, reward: { type: 'reward', amount: 1 } }),
addListener: jest.fn(() => ({ remove: jest.fn() })),
setPrivacy: jest.fn(),
setLogLevel: jest.fn(),
},
QartveloAdsBanner: () => null,
isQartveloAdsError: () => false,
}));
```
Without a mock it behaves as on iOS (the Jest preset reports iOS): promises reject with `unsupported_platform` and the banner renders nothing.
# Events and errors
> Subscribe to SDK events and handle QartveloAdsError codes in React Native.
## Events [Section titled “Events”](#events) `QartveloAds.addListener(type, listener)` observes events of every placement and format, banners included. Remove subscriptions when the screen goes away:
```tsx
useEffect(() => {
const subscriptions = [
QartveloAds.addListener('fallbackStarted', (e) => log(`fallback ${e.placementId}: ${e.reason}`)),
QartveloAds.addListener('impression', (e) => analytics.track('ad_impression', { placement: e.placementId, source: e.source })),
];
return () => subscriptions.forEach((s) => s.remove());
}, []);
```
| Event | When | Extra fields | | ----------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `loaded` | An ad is ready (Qartvelo Ads, or AdMob after a fallback) | `source`, `campaignId?`, `creativeId?` | | `loadFailed` | A load or show failed | `error: { code, message }`; `format` may be missing for a placement this app never loaded | | `shown` | The ad is on screen | `source`, ids | | `impression` | The impression was counted | `source`, ids | | `clicked` | The user tapped the ad (at most once per ad) | `source`, ids | | `dismissed` | A full-screen ad closed | `source`, ids | | `rewarded` | Completion confirmed for a rewarded ad | `source`, `reward: { type, amount }` | | `fallbackStarted` | Qartvelo Ads could not serve; the fallback is being tried | `reason`: `no_fill`, `timeout`, `error`, `creative_failed`, `disabled` | | `noAdAvailable` | No source has an ad | | Every payload has `type`, `placementId` and `format` (`banner`, `interstitial` or `rewarded`). `source` is `qartvelo` or `admob`; `campaignId` and `creativeId` are set only for Qartvelo Ads ads. Each `addListener` call is an independent subscription; `remove()` is safe to call twice. `QartveloAds.removeAllListeners(type?)` clears listeners in bulk. The native event stream is open only while at least one listener exists. Payloads are fully typed through `QartveloAdsEventMap`. ## Errors [Section titled “Errors”](#errors) Every promise rejects with a `QartveloAdsError` that has `code`, `message`, and `placementId` for placement calls. Narrow with `isQartveloAdsError`:
```ts
import { isQartveloAdsError } from '@qartvelo/react-native-ads';
try {
await QartveloAds.loadRewarded('reward_coins');
} catch (error) {
if (isQartveloAdsError(error) && error.code === 'no_fill') {
hideRewardButton();
}
}
```
| Code | Meaning | | --------------------------- | --------------------------------------------------------------------------------- | | `not_initialized` | `initialize()` was not called, or the backend rejected the app key / package name | | `invalid_placement` | Unknown placement code, or a placement used with the wrong format | | `no_fill` | Neither Qartvelo Ads nor the fallback had an ad | | `timeout` / `network_error` | The backend was slow or unreachable and no fallback was available | | `creative_failed` | The creative could not be downloaded or decoded | | `already_showing` | Another full-screen ad is on screen | | `show_failed` | The ad could not be displayed (for example no foreground Activity) | | `ad_expired` | The ad expired before it was shown | | `internal_error` | Unexpected native failure, or an unknown code | | `invalid_argument` | A JavaScript argument was rejected before reaching native code | | `unsupported_platform` | Called on a platform without the SDK (iOS, web) | | `module_unavailable` | The native module is missing: rebuild the Android app |
# Installation
> Install @qartvelo/react-native-ads and configure the Android build.
`@qartvelo/react-native-ads` brings Qartvelo Ads banner, interstitial and rewarded ads to React Native apps on Android. It is a thin bridge: campaign selection, timeouts, AdMob fallback, request de-duplication and the reward-once guarantee all run in the native [Kotlin SDK](/android/installation/). The JavaScript layer validates arguments, returns typed results and forwards events. * New Architecture only: a TurboModule (`NativeQartveloAds`) and a Fabric component (`QartveloAdsBannerView`), generated by codegen and autolinked. * Android only for now. On iOS and web every promise rejects with `unsupported_platform`, `` renders nothing and `QartveloAds.isSupported()` returns `false`, so shared code runs everywhere. ## Requirements [Section titled “Requirements”](#requirements) | | | | ------------ | ------------------------------------------------------------------------------------ | | React Native | 0.79 or newer with the New Architecture enabled (`newArchEnabled=true`, the default) | | Android | minSdk 24, compileSdk 35 or newer | | Build JDK | 17 | | Expo | Bare workflow or a development build (`expo prebuild`); not Expo Go | Tested with React Native 0.87.1 (React 19.2). ## 1. Install the package [Section titled “1. Install the package”](#1-install-the-package)
```sh
npm install @qartvelo/react-native-ads
# or: yarn add @qartvelo/react-native-ads
```
## 2. Add JitPack [Section titled “2. Add JitPack”](#2-add-jitpack) The plugin depends on the native SDK `com.qartvelo.ads:core:0.3.0` from JitPack. Add the repository to every project: android/build.gradle
```groovy
allprojects {
repositories {
maven {
url "https://jitpack.io"
content { includeGroup("com.qartvelo.ads") }
}
}
}
```
## 3. Enable the AdMob fallback (optional) [Section titled “3. Enable the AdMob fallback (optional)”](#3-enable-the-admob-fallback-optional) By default only the core SDK is included and a Qartvelo Ads no-fill simply reports “no ad”. To fall back to your own AdMob units: android/gradle.properties
```properties
QartveloAds_admobEnabled=true
# Optional: pin a different native SDK version (default 0.3.0)
# QartveloAds_sdkVersion=0.3.0
```
This adds `com.qartvelo.ads:admob`, which brings Google’s `play-services-ads` (25.4.0). Google requires your AdMob App ID in the manifest, or the app crashes at start-up: android/app/src/main/AndroidManifest.xml
```xml
```
Google’s sample App ID `ca-app-pub-3940256099942544~3347511713` works for development. ## 4. Rebuild [Section titled “4. Rebuild”](#4-rebuild) The TurboModule and banner component are compiled into the app, so rebuild after installing or upgrading:
```sh
npx react-native run-android
```
A JavaScript reload is not enough: calls reject with `module_unavailable` until the native app is rebuilt. ## Local backend over HTTP (debug only) [Section titled “Local backend over HTTP (debug only)”](#local-backend-over-http-debug-only) For a local backend at `http://10.0.2.2:8000/`, allow cleartext in debug builds: android/app/src/debug/res/xml/network_security_config.xml
```xml
10.0.2.2
localhost
```
android/app/src/debug/AndroidManifest.xml
```xml
```
`localhost` keeps Metro working on physical devices through `adb reverse tcp:8081 tcp:8081`. Next: [Usage](/react-native/usage/).
# Troubleshooting
> Fixes for common React Native build and runtime problems.
| Symptom | Fix | | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `Could not find com.qartvelo.ads:core:0.3.0` | Add JitPack to `allprojects.repositories` in `android/build.gradle` ([Installation](/react-native/installation/#2-add-jitpack)) | | App crashes at start: “The Google Mobile Ads SDK was initialized incorrectly” | `QartveloAds_admobEnabled=true` without the AdMob `APPLICATION_ID` meta-data. Add it, or disable the adapter | | Promises reject with `module_unavailable` | The app binary predates the package. Rebuild with `npx react-native run-android` | | Promises reject with `unsupported_platform` | Running on iOS or web; only Android has an SDK today. Guard with `QartveloAds.isSupported()` | | `initialize` rejects with `network_error` on the emulator (local backend) | Use `http://10.0.2.2:/`, not `localhost`, and allow cleartext for it in debug | | `initialize` rejects with `not_initialized` | The backend rejected the app key: check the key, that the package name equals your `applicationId`, and that the app is approved (or use `testMode`) | | Options seem ignored | Initialization happens once per process; force-stop and restart the app after changing options | | Every load ends with `fallbackStarted` reason `timeout` | The backend answered after the request timeout. Common on slow debug networks; raise the placement timeout in the dashboard while testing | | `fallbackStarted` but no AdMob ad | `QartveloAds_admobEnabled` is not set, `admobFallback` is false, the placement’s fallback is `none`, or no AdMob unit is configured for it | | Banner stays empty | Check `onLoadFailed`; give the component a width (it defaults to `100%`) and make sure only one banner per placement code is visible | | Two copies of React in Metro (local package) | Point Metro’s `watchFolders` at the package and block its `node_modules/react(-native)`, as in the example app’s `metro.config.js` | More in the general [Troubleshooting](/resources/troubleshooting/) page.
# Usage
> Initialize, show banners, interstitials and rewarded ads from React Native.
## Initialize [Section titled “Initialize”](#initialize) Call `initialize` once, as early as possible, for example in your root component. App.tsx
```tsx
import { useEffect } from 'react';
import { QartveloAds, isQartveloAdsError } from '@qartvelo/react-native-ads';
export default function App() {
useEffect(() => {
QartveloAds.setPrivacy({ consentGiven: undefined }); // see Privacy guide
QartveloAds.initialize({
appKey: 'app_xxxxxxxxxxxxxxxxxxxxxxxx',
testMode: __DEV__,
logLevel: __DEV__ ? 'debug' : 'error',
}).catch((error) => {
// Not fatal: the SDK keeps working on its cached config and the AdMob fallback.
if (isQartveloAdsError(error)) console.warn(error.code, error.message);
});
}, []);
return ;
}
```
| Option | Type | Default | Meaning | | ------------------ | ---------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------ | | `appKey` | `string` | required | Publisher app key (`app_...`) | | `baseUrl` | `string` | `https://ads.qartvelo.com/` | API origin; change only for a self-hosted or local backend | | `requestTimeoutMs` | `number` | `800` | Qartvelo Ads time budget before falling back. The dashboard value per placement wins | | `testMode` | `boolean` | `false` | Non-billable test ads; AdMob uses Google’s test units | | `testForceNoFill` | `boolean` | `false` | Force Qartvelo Ads “no fill” to see the AdMob fallback | | `admobFallback` | `boolean` | `true` | Allow the AdMob adapter (needs `QartveloAds_admobEnabled=true`) | | `logLevel` | `'none' \| 'error' \| 'info' \| 'debug'` | `'error'` | Logcat verbosity (tag `QartveloAds`) | | `admobAdUnits` | `Record` | `{}` | Placement code to AdMob unit id; overrides the dashboard | * Initialization is idempotent for the life of the **process**. Later calls resolve or reject with the first result and ignore new options. Restart the app to change them; a JS reload is not enough. * A rejection (`network_error`, `timeout`, or `not_initialized` for a rejected key) does not disable ads. * Loads issued while initialization is still running wait for it. * `await QartveloAds.isInitialized()` is `true` once the first attempt finished. ## Banner [Section titled “Banner”](#banner)
```tsx
import { QartveloAdsBanner } from '@qartvelo/react-native-ads';
console.log('banner from', e.source)}
onLoadFailed={(e) => console.log(e.error.code)}
onNoAdAvailable={() => setShowBanner(false)}
/>
```
* Collapsed (height 0) until an ad is rendered, then takes the creative’s height. Set an explicit `height` to reserve space instead. * Props: `placementId` (required), `style`, `onLoaded`, `onLoadFailed`, `onShown`, `onImpression`, `onClicked`, `onFallbackStarted`, `onNoAdAvailable`, `onSizeChange`. * Re-renders never reach the SDK. Changing `placementId` loads the new placement. * Remounting (navigation, list recycling) does not request a new ad: the SDK keeps one banner per placement and the new view re-attaches. The component’s `onLoaded` fires again for the new view; global listeners do not get a second `loaded`. * Refresh follows the placement’s `banner_refresh_seconds` (minimum 30 s) and pauses while off screen or in the background. * One visible banner per placement code at a time. * A banner mounted before `initialize()` waits and loads as soon as initialization starts. ## Interstitial [Section titled “Interstitial”](#interstitial)
```tsx
async function onLevelComplete() {
try {
const result = await QartveloAds.showInterstitial('game_end'); // resolves on dismiss
if (!result.shown) {
// nothing was ready
}
} catch (error) {
// already_showing, show_failed, ...
} finally {
continueGame();
QartveloAds.loadInterstitial('game_end').catch(() => {}); // preload the next one
}
}
// Preload when the level starts
QartveloAds.loadInterstitial('game_end').catch(() => {});
```
* `loadInterstitial` resolves with `AdInfo` (`{ placementId, format, source, campaignId?, creativeId? }`) when an ad from Qartvelo Ads or AdMob is ready, and rejects when no source has one. Concurrent loads share one request. * `showInterstitial` resolves `{ shown: true, source }` after the ad is dismissed, or `{ shown: false }` immediately when nothing was ready. It rejects with `already_showing` while another full-screen ad is visible and with `show_failed` if the ad could not be displayed. * `isInterstitialReady(placementId)` resolves whether a show would display an ad now. * A Qartvelo Ads ad is valid for 30 minutes after loading. ## Rewarded [Section titled “Rewarded”](#rewarded)
```tsx
await QartveloAds.loadRewarded('reward_coins');
const result = await QartveloAds.showRewarded('reward_coins');
if (result.rewarded) {
grantCoins(50);
}
```
* `result.rewarded` is `true` exactly once per show and only after the SDK confirmed completion, whichever network served the ad. A skipped ad resolves `{ shown: true, rewarded: false }`. * `result.reward` is `{ type, amount }`: `{ type: 'reward', amount: 1 }` for Qartvelo Ads, your AdMob unit’s settings for AdMob. * Grant the reward from the promise result **or** the `rewarded` event, never from both. ## Hooks pattern [Section titled “Hooks pattern”](#hooks-pattern)
```tsx
import { useCallback, useEffect, useState } from 'react';
import { QartveloAds } from '@qartvelo/react-native-ads';
export function useRewarded(placementId: string) {
const [ready, setReady] = useState(false);
const load = useCallback(() => {
setReady(false);
QartveloAds.loadRewarded(placementId).then(() => setReady(true), () => setReady(false));
}, [placementId]);
useEffect(load, [load]);
const show = useCallback(async () => {
const result = await QartveloAds.showRewarded(placementId);
load();
return result.rewarded;
}, [placementId, load]);
return { ready, show };
}
```
## Privacy and logs [Section titled “Privacy and logs”](#privacy-and-logs)
```ts
QartveloAds.setPrivacy({ consentGiven: false, childDirected: true });
QartveloAds.setLogLevel('debug');
```
Omitted privacy fields stay unknown; consent is never assumed. See [Privacy](/guides/privacy/).
# Agent skill and rules
> Drop-in instructions for Claude Code, Cursor, GitHub Copilot, Codex and other coding agents.
One file, [`SKILL.md`](/skills/qartvelo-ads/SKILL.md), teaches an agent the exact package names, the integration steps, the rules (test mode, reward once, continue after every show) and the release checklist. Install it in the format your tool reads. * Claude Code Claude Code loads skills from `.claude/skills//SKILL.md` in your project (or `~/.claude/skills/` for all projects) and uses them automatically when relevant:
```sh
mkdir -p .claude/skills/qartvelo-ads
curl -sL https://developers.qartvelo.com/skills/qartvelo-ads/SKILL.md \
-o .claude/skills/qartvelo-ads/SKILL.md
```
* Cursor
```sh
mkdir -p .cursor/rules
{ printf -- '---\ndescription: Qartvelo Ads SDK integration rules\nglobs: ["**/*.kt", "**/*.kts", "**/*.tsx", "**/*.ts", "**/AndroidManifest.xml"]\nalwaysApply: false\n---\n'; \
curl -sL https://developers.qartvelo.com/skills/qartvelo-ads/SKILL.md | sed '1,/^---$/d'; } \
> .cursor/rules/qartvelo-ads.mdc
```
* GitHub Copilot Append the instructions to the repository’s custom instructions:
```sh
mkdir -p .github
curl -sL https://developers.qartvelo.com/skills/qartvelo-ads/SKILL.md \
| sed '1,/^---$/d' >> .github/copilot-instructions.md
```
* AGENTS.md Codex, Jules, Aider, Zed and other agents read `AGENTS.md` at the repository root:
```sh
curl -sL https://developers.qartvelo.com/skills/qartvelo-ads/SKILL.md \
| sed '1,/^---$/d' >> AGENTS.md
```
The `sed` commands strip the skill’s YAML front matter for tools that do not use it. ## What the skill contains [Section titled “What the skill contains”](#what-the-skill-contains) * Exact coordinates: `com.qartvelo.ads:core:0.3.0`, `com.qartvelo.ads:admob:0.3.0`, `@qartvelo/react-native-ads`, JitPack. * Exact names that models get wrong: `AdSource.QARTVELO`, `app:qartvelo_placementId`, `QartveloAds_admobEnabled`. * The seven integration steps, from dependencies to verifying test ads. * Rules: test mode in debug, reward only on completion, continue after every terminal callback, never embed the SDK secret, one banner per placement code. * Error codes and the release checklist. Combine the skill with the [MCP server](/ai/mcp-server/) for live docs search and test ad requests.
# llms.txt and Markdown
> Machine-readable versions of the documentation for LLMs and AI agents.
The site follows the [llms.txt](https://llmstxt.org/) convention. | URL | Contents | Use it for | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Index: project summary, key facts and links to the sets below | Point an agent at the docs | | [`/llms-full.txt`](/llms-full.txt) | Every page in one Markdown file | Paste into a long-context model or a project knowledge base | | [`/llms-small.txt`](/llms-small.txt) | Every page, with notes and tips removed | Smaller context windows | | [`/_llms-txt/android-sdk.txt`](/_llms-txt/android-sdk.txt) | Android SDK pages and the AdMob guide | Native Android projects | | [`/_llms-txt/react-native.txt`](/_llms-txt/react-native.txt) | React Native pages and the AdMob guide | React Native projects | | [`/_llms-txt/rest-api.txt`](/_llms-txt/rest-api.txt) | REST API pages | Custom clients | | `/.md` | One page as Markdown, for example [`/android/rewarded.md`](/android/rewarded.md) | Focused questions | | [`/openapi.yaml`](/openapi.yaml) | OpenAPI 3.1 description of the SDK REST API | API clients, code generators, API tools | | [`/skills/qartvelo-ads/SKILL.md`](/skills/qartvelo-ads/SKILL.md) | Agent skill | Claude and other skill-aware agents | ## Examples [Section titled “Examples”](#examples) Add the docs to a Cursor chat with `@Docs` and the URL `https://developers.qartvelo.com/llms-full.txt`, or in any assistant:
```text
Read https://developers.qartvelo.com/_llms-txt/android-sdk.txt and add a rewarded ad for the
placement reward_coins to ShopActivity. Grant 50 coins only on confirmed completion.
```
Fetch from a script:
```sh
curl -s https://developers.qartvelo.com/llms-full.txt -o qartvelo-ads-docs.md
```
The files are regenerated on every docs deployment, so they always match the current SDK version.
# MCP server
> Give AI agents Qartvelo Ads tools - docs search, code generation, error lookup and test ad requests - over the Model Context Protocol.
`@qartvelo/ads-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. It runs locally over stdio, bundles these docs (so search works offline), and exposes tools your agent can call while it edits your project. ## Tools [Section titled “Tools”](#tools) | Tool | What it does | Network | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `search_docs` | Ranked keyword search over the docs; returns sections, excerpts and paths | No | | `read_doc` | Returns a full page as Markdown (`android/banner`, `api/ad-request`, …) | No | | `list_docs` | Lists all pages | No | | `generate_integration` | Gradle, manifest, initialization and per-placement code for an app key and placements, for Android or React Native. Flags common mistakes such as an AdMob App ID used as an ad unit id | No | | `explain_code` | Meaning and fix for any SDK error, API error, event rejection, no-fill or fallback reason | No | | `get_app_config` | Opens a **test-mode** session for an app key + package and returns the placements, fallback units, timeouts and kill switches | Yes | | `test_ad_request` | Test-mode session plus one ad request for a placement, with timings; tokens are redacted | Yes | It also provides every docs page as a resource (`qartvelo-docs://android/banner`) and a prompt, `integrate-qartvelo-ads`, that walks the agent through a full integration. ## Install [Section titled “Install”](#install) Requires Node.js 18 or newer. * Claude Code
```sh
claude mcp add qartvelo-ads -- npx -y @qartvelo/ads-mcp
```
Add `--scope project` to share it with your team through `.mcp.json`. * Cursor .cursor/mcp.json
```json
{
"mcpServers": {
"qartvelo-ads": {
"command": "npx",
"args": ["-y", "@qartvelo/ads-mcp"]
}
}
}
```
* VS Code / Copilot .vscode/mcp.json
```json
{
"servers": {
"qartvelo-ads": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@qartvelo/ads-mcp"]
}
}
}
```
* Claude Desktop claude_desktop_config.json
```json
{
"mcpServers": {
"qartvelo-ads": {
"command": "npx",
"args": ["-y", "@qartvelo/ads-mcp"]
}
}
}
```
* Windsurf / others Any MCP client that supports stdio servers: command `npx`, arguments `-y @qartvelo/ads-mcp`. ### From source [Section titled “From source”](#from-source)
```sh
git clone https://github.com/Qartvelo-com/ads
cd ads/developers/mcp
npm install && npm run bundle-docs
claude mcp add qartvelo-ads -- node "$PWD/src/index.js"
```
### Configuration [Section titled “Configuration”](#configuration) | Environment variable | Default | Meaning | | ----------------------- | --------------------------- | ----------------------------------------------------------------------------------- | | `QARTVELO_ADS_BASE_URL` | `https://ads.qartvelo.com/` | API origin for `get_app_config` and `test_ad_request` (for example a local backend) | ## Example session [Section titled “Example session”](#example-session) > **You:** Add an interstitial after each level and a rewarded “double coins” button. App key `app_...`, package `com.example.game`. > > **Agent:** calls `get_app_config` and finds the placements `level_done` (interstitial) and `double_coins` (rewarded); calls `generate_integration` for Android; edits `settings.gradle.kts`, `app/build.gradle.kts`, `GameApp.kt`, `LevelActivity.kt` and `ShopFragment.kt`; runs `test_ad_request` for both placements to confirm a `fill` with the test creative; then reads `android/release-checklist` and reports what is left for release.
# Build with AI
> Use Claude, ChatGPT, Cursor, Copilot and other AI agents to integrate Qartvelo Ads correctly on the first try.
These docs are designed to be read by AI assistants as well as people. Pick the integration that fits your tools: [llms.txt and Markdown](/ai/llms-txt/)The whole site as plain text for any LLM, plus every page as .md. [MCP server](/ai/mcp-server/)Let Claude Code, Cursor, VS Code or Claude Desktop search the docs, generate code and send test ad requests. [Agent skill and rules](/ai/agent-skill/)A SKILL.md for Claude, plus ready-made Cursor rules, AGENTS.md and Copilot instructions. [Prompts](/ai/prompts/)Copy-paste prompts for integrating, migrating from AdMob, debugging and reviewing. ## On every page [Section titled “On every page”](#on-every-page) Each page has three actions under its title: * **Copy page** copies the page as Markdown, ready to paste into any chat. * **View as Markdown** opens the raw `.md` (append `.md` to any docs URL, for example [`/android/banner.md`](/android/banner.md)). * **Ask AI about this page** opens ChatGPT, Claude or Perplexity with the page already referenced. ## Why this matters [Section titled “Why this matters”](#why-this-matters) Ad SDKs have details an LLM cannot guess: the exact Gradle coordinates, the XML attribute name, which callback ends a show, when it is safe to grant a reward. Models trained on older or similar SDKs will confidently write `ourads_placementId`, `AdSource.OURADS` or grant rewards in `onDismissed`. Giving the agent these docs, the skill or the MCP server avoids those mistakes.
# Prompts
> Copy-paste prompts for common Qartvelo Ads tasks with AI coding assistants.
Replace the placeholders in angle brackets. Each prompt points the assistant at the right docs, so it works with any tool that can read URLs; with the [MCP server](/ai/mcp-server/) installed the agent uses its tools instead. ## Integrate from scratch [Section titled “Integrate from scratch”](#integrate-from-scratch)
```text
Integrate the Qartvelo Ads SDK into this Android app.
Docs: https://developers.qartvelo.com/_llms-txt/android-sdk.txt
App key: . Placements: .
- Initialize in the Application class with testMode = BuildConfig.DEBUG.
- Banner on , interstitial after , rewarded behind the <"Extra life"> button.
- Continue the flow from onDismissed, onNoAdAvailable and onLoadFailed. Grant rewards only in onReward.
- Keep my existing AdMob App ID and ad units as the fallback.
Show me the diff and anything I must do in the dashboard.
```
## React Native [Section titled “React Native”](#react-native)
```text
Add @qartvelo/react-native-ads to this React Native app following
https://developers.qartvelo.com/_llms-txt/react-native.txt.
App key , placements .
Enable the AdMob fallback (QartveloAds_admobEnabled=true) and use testMode: __DEV__.
Create a small hook for the rewarded ad that exposes ready and show().
```
## Migrate from AdMob only [Section titled “Migrate from AdMob only”](#migrate-from-admob-only)
```text
This app shows AdMob ads directly. Put Qartvelo Ads in front of them without losing AdMob revenue:
read https://developers.qartvelo.com/guides/admob-fallback.md, create a placement code for each
existing AdMob ad unit, map them with admobAdUnits, and replace the direct AdMob load/show calls with
QartveloAds calls. Keep the AdMob App ID in the manifest. List the placements I need to create in the
dashboard with their format and ad unit id.
```
## Debug [Section titled “Debug”](#debug)
```text
Qartvelo Ads is not showing ads. Here is `adb logcat -s QartveloAds` with DEBUG logging:
Use https://developers.qartvelo.com/resources/troubleshooting.md and the error codes in
https://developers.qartvelo.com/android/events.md to find the cause and the fix.
```
## Review before release [Section titled “Review before release”](#review-before-release)
```text
Review this project's Qartvelo Ads integration against
https://developers.qartvelo.com/android/release-checklist.md and
https://developers.qartvelo.com/guides/privacy.md. Report every item that fails, with file and line.
```
## Build a custom client [Section titled “Build a custom client”](#build-a-custom-client)
```text
Write a client for the Qartvelo Ads REST API from
https://developers.qartvelo.com/openapi.yaml and https://developers.qartvelo.com/_llms-txt/rest-api.txt.
Follow the "Client requirements" section exactly (impression once on screen, click after a tap, reward
only on rewarded placements, treat rejections as final, honour Retry-After).
```
# POST /ads/request
> Request an ad for a placement and receive a fill with a signed impression token, or a no-fill.
```http
POST /api/v1/ads/request
Content-Type: application/json
```
## Request [Section titled “Request”](#request) | Field | Type | Required | Notes | | ------------------------------- | ---------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | `app_key` | string | yes | Must be the app the session was issued for | | `placement` | string, max 64 | yes | Placement code (`[a-z0-9_]{2,64}`) | | `format` | `banner` \| `interstitial` \| `rewarded` | yes | Must match the placement’s format | | `session_token` | string | yes | From `/sdk/initialize` | | `language` | string, max 35 | no | Content language such as `ka` or `en-US` (primary subtag used). Defaults to the app’s language | | `android_version` | string | no | `14` or `14.0.1`; the major version is used for targeting | | `app_version`, `sdk_version` | string | no | Informational | | `screen_width`, `screen_height` | integer 0..20000 | no | Pixels. Used to pick a banner that fits and an interstitial matching the orientation | | `test_mode` | boolean | no | Serve the built-in test ad (also implied by a test session) | | `test_force_no_fill` | boolean | no | Always answer `no_fill` with reason `test_no_fill` |
```json
{"app_key":"app_xxxxxxxxxxxxxxxxxxxxxxxx","placement":"game_end","format":"interstitial",
"session_token":"","language":"ka","android_version":"14","app_version":"1.0.0",
"sdk_version":"0.3.0","screen_width":1080,"screen_height":2400}
```
## Response 200: fill [Section titled “Response 200: fill”](#response-200-fill)
```json
{
"status": "fill",
"request_id": "req_01k9xyz...",
"ad": {
"id": "ad_3f2a9c01d4e5b6a7",
"campaign_id": "cmp_12",
"creative_id": "cr_34",
"format": "interstitial",
"creative_type": "image",
"creative_url": "https://cdn.example.com/creatives/12/01k....png",
"click_url": "https://advertiser.example",
"width": 1080,
"height": 1920,
"duration_seconds": null,
"impression_token": "",
"expires_at": "2026-10-07T18:40:00Z",
"test": false
}
}
```
| Field | Meaning | | ----------------------- | -------------------------------------------------------------------------------- | | `request_id` | Send with every event for this ad | | `ad.creative_type` | `image` (PNG, JPEG, WebP, GIF) or `video` (MP4, H.264) | | `ad.creative_url` | Download before reporting the ad as loaded | | `ad.click_url` | Advertiser landing page, opened on tap after the click event is queued | | `ad.width`, `ad.height` | Creative size in pixels | | `ad.duration_seconds` | Video length, `null` for images | | `ad.impression_token` | Opaque, signed, single-use | | `ad.expires_at` | 30 minutes after the request. Never show the ad after this | | `ad.test` | `true` for test ads (`campaign_id` `cmp_test`, `creative_id` `cr_test_{format}`) | ## Response 200: no fill [Section titled “Response 200: no fill”](#response-200-no-fill)
```json
{"status":"no_fill","request_id":"req_01k9xyz...","fallback":"admob","reason":"no_eligible_campaign"}
```
`fallback` is the placement’s provider: `admob` or `none`. | Reason | Meaning | | ---------------------- | -------------------------------------------------------------------------------------- | | `no_eligible_campaign` | No campaign matched, or none had budget left | | `serving_disabled` | Global, publisher or app kill switch (or publisher/app not approved outside test mode) | | `placement_disabled` | Placement paused, switched off, or disabled by an admin | | `frequency_capped` | The placement’s frequency cap for this session is reached | | `test_no_fill` | `test_force_no_fill` was set | ## Selection [Section titled “Selection”](#selection) 1. Session verified, app key matches the session, app approved (unless test). 2. Placement found and format matches; kill switches; placement frequency cap. 3. Test mode returns the test ad. Otherwise: 4. Eligible campaigns: active, within schedule, budget left, advertiser approved with balance, country matches (or any), at least one approved creative of the requested format. 5. Targeting: formats, languages, app ids, app categories, Android versions. Empty means any. A campaign that targets Android versions is skipped when the request has none. 6. Campaign frequency cap for this session. 7. Highest CPM bid first (ties random), with daily budget pacing. 8. The cost of one impression is reserved atomically; if a campaign cannot reserve, the next one is tried. Country comes from the network address at Qartvelo Ads’ edge, never from the device. The IP address is not stored. ## Errors [Section titled “Errors”](#errors) | HTTP | Code | When | | ---- | --------------------- | ------------------------------------------------------ | | 401 | `invalid_session` | Token malformed, forged or issued for another app | | 401 | `session_expired` | Initialize again | | 403 | `app_not_approved` | App not approved and not a test session | | 404 | `placement_not_found` | No placement with that code in this app | | 422 | `format_mismatch` | `format` differs from the placement | | 422 | `validation_failed` | Missing or invalid fields | | 429 | `rate_limited` | See [rate limits](/api/errors-and-limits/#rate-limits) |
# Errors and limits
> Error envelope and codes, rate limits and fraud rules of the Qartvelo Ads API.
## Error envelope [Section titled “Error envelope”](#error-envelope) Every non-2xx response except event rejections uses:
```json
{"error":{"code":"validation_failed","message":"The placement field is required.","fields":{"placement":["The placement field is required."]}}}
```
`fields` is present only for `validation_failed`. Branch on `code`; `message` is human-readable and may change. | Code | HTTP | When | What to do | | --------------------- | ---- | -------------------------------------------------- | ------------------------------------------- | | `invalid_app_key` | 401 | Unknown app key | Check the key on the app page | | `invalid_session` | 401 | Session token malformed, forged or for another app | Initialize again | | `session_expired` | 401 | Session token expired | Initialize again | | `package_mismatch` | 403 | Package name differs from the registered app | Fix the `applicationId` or the registration | | `app_not_approved` | 403 | App not approved and not in test mode | Use test mode until approved | | `placement_not_found` | 404 | No placement with that code in this app | Create it, or fix the code | | `format_mismatch` | 422 | Requested format differs from the placement | Use the matching load method | | `validation_failed` | 422 | Missing or invalid fields | See `error.fields` | | `rate_limited` | 429 | A rate limit was exceeded | Wait for `Retry-After` seconds | | `server_error` | 500 | Unexpected error | Retry with backoff; fall back | Event endpoints answer rejections with `{"status":"rejected","reason":"..."}` instead; see [Events](/api/events/). ## Rate limits [Section titled “Rate limits”](#rate-limits) Per minute. “Network” means the client’s IPv4 /24 or IPv6 /48 (hashed). Network limits are generous because mobile carriers put many users behind one carrier-grade NAT. | Endpoint | Dimension | Limit | | --------------------------------------- | ----------------- | ----------- | | `/sdk/initialize` | network | 600 | | `/sdk/initialize` | app key | 3000 | | `/ads/request` | session | 120 | | `/ads/request` | network | 6000 | | `/ads/request` | app key | 20000 | | `/events/impression`, `click`, `reward` | network | 12000 | | `/events/impression`, `click`, `reward` | request id | 20 | | `/events/fallback` | session / network | 120 / 12000 | A `429` carries a `Retry-After` header. Contact Qartvelo Ads if a large app needs higher app-key limits. ## Fraud rules [Section titled “Fraud rules”](#fraud-rules) Traffic that breaks these rules is not billed and does not earn revenue. Rejections are logged for review. | Rule | Default | Effect | | ------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------- | | Ad requests per session per minute | 30 | Further requests get `no_fill` | | Impressions per placement per session per hour | Banners: `3600 / refresh seconds x 1.5` (180 for a 30 s banner). Full-screen: 60 | Impression rejected `suspicious` | | Impressions per session per hour, all placements | 720 | Impression rejected `suspicious` | | Time between impression and click | At least 1 s | Click rejected `suspicious` | | Clicks per session per hour | 20 | Click rejected `suspicious` | | Session click-through rate | Above 50% once the session has 10+ impressions | Click rejected `suspicious` | | Replays and duplicates | Always | `409 duplicate` | | Package mismatch | Always | `403 package_mismatch` |
# Events
> Report impressions, clicks, rewards and fallbacks. Every event is validated, de-duplicated and fraud-checked.
## POST /events/impression, /events/click [Section titled “POST /events/impression, /events/click”](#post-eventsimpression-eventsclick)
```json
{"request_id":"req_01k9xyz...","impression_token":""}
```
| Field | Type | Required | | ------------------ | ---------------- | -------- | | `request_id` | string, max 64 | yes | | `impression_token` | string, max 2048 | yes | ## POST /events/reward [Section titled “POST /events/reward”](#post-eventsreward) Adds a required `completion` flag:
```json
{"request_id":"req_01k9xyz...","impression_token":"","completion":true}
```
## Responses [Section titled “Responses”](#responses) | Response | Meaning | | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | | 200 `{"status":"accepted"}` | Impression or click recorded | | 200 `{"status":"accepted","rewarded":true}` | Reward recorded; `rewarded` mirrors `completion` | | 409 `{"status":"rejected","reason":"duplicate"}` | This token was already used for this event type | | 422 `{"status":"rejected","reason":"invalid_token"}` | Bad signature or payload, wrong token type, or a reward on a non-rewarded placement | | 422 `{"status":"rejected","reason":"expired_token"}` | Impression after the ad expired, or click/reward more than one hour after that | | 422 `{"status":"rejected","reason":"request_mismatch"}` | The token belongs to another `request_id` | | 422 `{"status":"rejected","reason":"no_impression"}` | Click or reward without an accepted impression for this token | | 422 `{"status":"rejected","reason":"suspicious"}` | A fraud rule rejected the event | | 422 `{"error":{"code":"validation_failed",...}}` | Missing fields | | 429 `{"error":{"code":"rate_limited",...}}` | Rate limited | Treat every `rejected` response as **final**: retrying cannot succeed. Rejected events are never billed. ## Rules [Section titled “Rules”](#rules) * **Impression**: accepted only before the ad’s `expires_at`, exactly once per token. This is the billing event: the campaign is charged one impression (CPM bid / 1000) and the publisher is credited their revenue share. Test tokens are validated the same way but never billed. * **Click**: requires the accepted impression for the same request and token, at most one per impression, up to one hour after the ad’s expiry. Clicks are counted, not billed. * **Reward**: requires the accepted impression on a **rewarded** placement; only one reward event per impression is ever stored and the first report wins (`completion:false` cannot be upgraded later). Grant the in-app reward on confirmed completion; the server response is for reporting and auditing. ## POST /events/fallback [Section titled “POST /events/fallback”](#post-eventsfallback) Fire-and-forget telemetry that the client fell back to another network. It feeds the **Fallbacks** column in publisher reports. Test sessions are ignored.
```json
{"session_token":"","placement":"game_end","reason":"timeout"}
```
| Field | Values | | --------------- | ------------------------------------------------ | | `session_token` | From `/sdk/initialize` | | `placement` | Placement code | | `reason` | `no_fill`, `timeout`, `error`, `creative_failed` | Response 200 `{"status":"accepted"}`. Errors: `invalid_session` / `session_expired` 401, `placement_not_found` 404, `validation_failed` 422. The SDK maps its internal `disabled` reason to `no_fill`.
# POST /sdk/initialize
> Validate the app key and package name, and receive a session token and remote configuration.
```http
POST /api/v1/sdk/initialize
Content-Type: application/json
```
## Request [Section titled “Request”](#request) | Field | Type | Required | Notes | | -------------- | --------------- | -------- | ----------------------------------------------------------------- | | `app_key` | string, max 64 | yes | `app_` + 24 alphanumerics, from the publisher dashboard | | `package_name` | string, max 255 | yes | Android application id, compared exactly with the registered one | | `sdk_version` | string, max 32 | no | | | `app_version` | string, max 64 | no | | | `platform` | string | no | only `android` is accepted | | `os_version` | string, max 32 | no | | | `test_mode` | boolean | no | Skips the app approval check; the session only ever gets test ads |
```json
{"app_key":"app_xxxxxxxxxxxxxxxxxxxxxxxx","package_name":"com.example.app","sdk_version":"0.3.0",
"app_version":"1.0.0","platform":"android","os_version":"14","test_mode":false}
```
## Response 200 [Section titled “Response 200”](#response-200)
```json
{
"session_token": "",
"session_expires_at": "2026-10-07T19:40:00Z",
"config": {
"serving_enabled": true,
"request_timeout_ms": 800,
"fallback_enabled": true,
"test_mode": false,
"config_ttl_seconds": 3600
},
"placements": [
{
"code": "game_end",
"format": "interstitial",
"ourads_enabled": true,
"fallback_provider": "admob",
"admob_ad_unit_id": "ca-app-pub-3940256099942544/1033173712",
"request_timeout_ms": 800,
"frequency_cap_count": null,
"frequency_cap_period": null,
"banner_refresh_seconds": 60
}
]
}
```
| Field | Meaning | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session_token` | Opaque, signed. Send it with ad requests and fallback telemetry. Valid until `session_expires_at` (one hour) | | `config.serving_enabled` | `false` when a kill switch applies to the whole app (global, publisher, or app). Go straight to the fallback | | `config.request_timeout_ms` | Default request budget for placements | | `config.fallback_enabled` | Whether fallback is allowed at all; decide per placement with `fallback_provider` | | `config.test_mode` | Echo of the request. Clients must take test mode from their local option and never cache it | | `config.config_ttl_seconds` | How long the client may cache this configuration | | `placements[].code` / `format` | Placement code and its format | | `placements[].ourads_enabled` | `false` when the placement is paused, switched off by the publisher or an admin, or the app is blocked. The name is historical; it means “Qartvelo Ads enabled”. Re-check at most every 5 minutes while off | | `placements[].fallback_provider` | `admob` or `none` | | `placements[].admob_ad_unit_id` | The publisher’s AdMob unit, or `null` | | `placements[].request_timeout_ms` | Per-placement request budget (100..5000) | | `placements[].frequency_cap_count` / `frequency_cap_period` | Placement cap per session id: `session`, `hour`, `day`, or `null` | | `placements[].banner_refresh_seconds` | Banner refresh interval, never below 30 | ## Errors [Section titled “Errors”](#errors) | HTTP | Code | When | | ---- | ------------------- | --------------------------------------------------------------------- | | 401 | `invalid_app_key` | Unknown app key | | 403 | `package_mismatch` | `package_name` differs from the registered app (logged as suspicious) | | 403 | `app_not_approved` | App not approved and `test_mode` is false | | 422 | `validation_failed` | Missing or invalid fields (`error.fields`) | | 429 | `rate_limited` | Too many initializations (see `Retry-After`) |
# REST API overview
> The HTTP API the SDKs use, for custom clients, server-side tests and debugging.
The official SDKs implement this API for you. Use it directly to build a client for another platform, to write integration tests, or to debug what the SDK sends. A machine-readable description is available as [OpenAPI 3.1](/openapi.yaml). | | | | -------------- | ------------------------------------------------------------------ | | Base URL | `https://ads.qartvelo.com/api/v1` | | Format | JSON request and response bodies, `Content-Type: application/json` | | Methods | All endpoints are `POST` | | Authentication | The public app key plus a session token; no cookies, no API keys | | Caching | Responses carry `Cache-Control: no-store, private` | ## Endpoints [Section titled “Endpoints”](#endpoints) | Endpoint | Purpose | | ----------------------------------------------------------- | --------------------------------------------------------------------------------- | | [`POST /sdk/initialize`](/api/initialize/) | Validate app key + package name, get a session token and the remote configuration | | [`POST /ads/request`](/api/ad-request/) | Get an ad for a placement, or an explicit no-fill with the fallback to use | | [`POST /events/impression`](/api/events/) | Record (and bill) one impression | | [`POST /events/click`](/api/events/) | Record one click for an accepted impression | | [`POST /events/reward`](/api/events/) | Record the rewarded outcome, exactly once | | [`POST /events/fallback`](/api/events/#post-eventsfallback) | Telemetry: the client fell back to another network | ## Flow [Section titled “Flow”](#flow)
```plaintext
initialize ──> session_token (1 h) + config
│
ads/request (placement, format, session_token) ──> fill: request_id + ad.impression_token
│ └─> no_fill: fallback = admob | none
impression (request_id, impression_token) once, while the ad is valid (30 min)
click (request_id, impression_token) at most once, after the impression
reward (request_id, impression_token, completion) rewarded placements only
```
## Conventions [Section titled “Conventions”](#conventions) * Timestamps are ISO-8601 UTC with `Z`, for example `2026-10-07T18:40:00Z`. * Public ids are prefixed strings: campaign `cmp_12`, creative `cr_34`, ad `ad_3f2a9c01d4e5b6a7`, request `req_01k9...`. * Session and impression tokens are **opaque**. Do not parse, modify or log them; pass them back verbatim. * Money never reaches the client. * Compress responses at the edge; keep HTTP keep-alive enabled. The SDKs use a single connection pool. * Errors use one envelope, except event rejections (see [Errors and limits](/api/errors-and-limits/)):
```json
{"error":{"code":"placement_not_found","message":"Placement [game_end] does not exist in this app."}}
```
## Try it with curl [Section titled “Try it with curl”](#try-it-with-curl) Every call below runs in test mode, so nothing is billed. Replace the app key and package with yours.
```bash
B=https://ads.qartvelo.com/api/v1
KEY=app_xxxxxxxxxxxxxxxxxxxxxxxx
PKG=com.example.app
# 1. Initialize in test mode
S=$(curl -s -X POST $B/sdk/initialize -H 'Content-Type: application/json' \
-d "{\"app_key\":\"$KEY\",\"package_name\":\"$PKG\",\"platform\":\"android\",\"test_mode\":true}" \
| jq -r .session_token)
# 2. Request an interstitial
AD=$(curl -s -X POST $B/ads/request -H 'Content-Type: application/json' \
-d "{\"app_key\":\"$KEY\",\"placement\":\"game_end\",\"format\":\"interstitial\",\"session_token\":\"$S\",\"test_mode\":true,\"screen_width\":1080,\"screen_height\":2400}")
echo "$AD" | jq .
RID=$(echo "$AD" | jq -r .request_id); TOK=$(echo "$AD" | jq -r .ad.impression_token)
# 3. Impression, then a click at least one second later
curl -s -X POST $B/events/impression -H 'Content-Type: application/json' \
-d "{\"request_id\":\"$RID\",\"impression_token\":\"$TOK\"}"
sleep 1
curl -s -X POST $B/events/click -H 'Content-Type: application/json' \
-d "{\"request_id\":\"$RID\",\"impression_token\":\"$TOK\"}"
# 4. A replay is rejected: {"status":"rejected","reason":"duplicate"} (HTTP 409)
curl -s -X POST $B/events/impression -H 'Content-Type: application/json' \
-d "{\"request_id\":\"$RID\",\"impression_token\":\"$TOK\"}"
# 5. Force a no-fill to exercise the fallback path
curl -s -X POST $B/ads/request -H 'Content-Type: application/json' \
-d "{\"app_key\":\"$KEY\",\"placement\":\"game_end\",\"format\":\"interstitial\",\"session_token\":\"$S\",\"test_mode\":true,\"test_force_no_fill\":true}"
# {"status":"no_fill","request_id":"req_...","fallback":"admob","reason":"test_no_fill"}
```
## Client requirements [Section titled “Client requirements”](#client-requirements) A custom client must behave like the SDKs to keep traffic valid: * Send the impression only when the creative is actually on screen, once, and before the ad’s `expires_at`. * Send a click only after a user tap, at most once per impression, and not within one second of the impression. * Send a reward event only for rewarded placements, once, with the real completion state; grant the reward only on completion. * Treat every `rejected` event response as final; retry only network errors, `5xx`, `408` and `429` (honour `Retry-After`). * Never show an ad after `expires_at`, and do not reuse a `request_id` or token. * Initialize again when a call returns `session_expired` or `invalid_session`.
# AdMob fallback
> How the SDK falls back to your own AdMob ad units, and how to set it up.
Qartvelo Ads is the primary ad source. When it has no eligible campaign, fails, is switched off or exceeds its time budget, the SDK can show an ad from **your own** Google AdMob account instead, so a placement is never left empty. ## Your AdMob account, your ad units, your revenue [Section titled “Your AdMob account, your ad units, your revenue”](#your-admob-account-your-ad-units-your-revenue) * You create your own AdMob account, your own AdMob app and your own ad units (one per placement and format) and give those ids to Qartvelo Ads. * Qartvelo Ads never owns, shares, proxies or pools AdMob inventory and never routes several publishers through one AdMob account. * Google pays AdMob revenue directly to you. Qartvelo Ads does not receive, report, take a share of, or have access to your AdMob revenue or account. * The adapter uses Google’s official `com.google.android.gms:play-services-ads` artifact as a normal Gradle dependency, never a modified copy. * You remain responsible for complying with AdMob policies (placement, invalid traffic, consent). ## Setup [Section titled “Setup”](#setup) 1. In AdMob, create an app for your package and ad units that match your placements: banner units for banner placements, interstitial units for interstitial placements, rewarded units for rewarded placements. 2. Add the AdMob **App ID** (contains `~`) to `AndroidManifest.xml` as `com.google.android.gms.ads.APPLICATION_ID` meta-data. 3. Add the adapter: `implementation("com.qartvelo.ads:admob:0.3.0")`, or `QartveloAds_admobEnabled=true` in React Native. 4. Map each placement to its ad unit **ID** (contains `/`), either on the placement in the dashboard (fallback provider **AdMob**) or in code:
```kotlin
QartveloAdsOptions(
admobAdUnits = mapOf(
"home_banner" to "ca-app-pub-XXX/111",
"game_end" to "ca-app-pub-XXX/222",
"reward_coins" to "ca-app-pub-XXX/333",
),
)
```
The in-code mapping wins over the dashboard value for the same placement code. No other code is needed. The SDK discovers the adapter automatically, initializes Google Mobile Ads once on a background thread and reports AdMob ads through the same callbacks with `info.source == AdSource.ADMOB` (`'admob'` in React Native). ## When a fallback is possible [Section titled “When a fallback is possible”](#when-a-fallback-is-possible) All of these must hold for a placement: * the adapter is present (or a custom adapter was registered with `QartveloAds.registerFallbackAdapter`); * `admobFallback` is `true` in the options and the remote config allows fallback; * the placement’s fallback provider is `admob` (and the ad response did not say `fallback: none`); * an ad unit id is known for the placement (in code or in the dashboard), or test mode is on. ## Decision flow [Section titled “Decision flow”](#decision-flow) **Interstitial and rewarded loads** 1. A still-valid cached Qartvelo Ads ad completes the load immediately. 2. Otherwise the SDK requests Qartvelo Ads and, if a fallback is possible, **preloads AdMob in parallel**. 3. Qartvelo Ads fill: the creative is downloaded and validated, then `onLoaded(source = QARTVELO)`. 4. No fill, timeout, network error, creative failure or kill switch: `onFallbackStarted(reason)`, then `onLoaded(source = ADMOB)` as soon as AdMob is ready, or `onNoAdAvailable` + `onLoadFailed` if AdMob also has nothing. 5. Show prefers a valid Qartvelo Ads ad, then a ready AdMob ad, else `onNoAdAvailable`. **Banners** request Qartvelo Ads first. On failure, the AdMob anchored adaptive banner is created in the same view and refreshes itself according to your AdMob settings. The next successful Qartvelo Ads refresh replaces it. ## Fallback reasons [Section titled “Fallback reasons”](#fallback-reasons) | Reason | Meaning | | ----------------- | ------------------------------------------------------------------ | | `no_fill` | The backend had no eligible campaign (or test mode forced it) | | `timeout` | No answer within the request timeout, or the creative was too slow | | `error` | Network or server error | | `creative_failed` | The creative could not be downloaded, decoded or rendered | | `disabled` | Qartvelo Ads is switched off for this placement, app or publisher | The SDK also sends a fire-and-forget `POST /api/v1/events/fallback` with the placement code and the reason (never any AdMob data), so your reports show fallback counts. ## Timeouts and remote configuration [Section titled “Timeouts and remote configuration”](#timeouts-and-remote-configuration) * The Qartvelo Ads request budget (session plus ad request) defaults to 800 ms. Users never wait for it: loads are asynchronous and AdMob is already preloading. * Effective timeout: the placement’s remote value, else `requestTimeoutMs`, clamped to 100..10000 ms. * Creative downloads have separate limits (images 10 s, video 45 s), because a Qartvelo Ads ad is only reported as loaded once its creative is on the device. * Without an app update, the dashboard can switch Qartvelo Ads off per placement (the SDK goes straight to AdMob), change the fallback provider, change the timeout and change the AdMob unit. The last configuration is cached for offline starts. * If the API is unreachable at start-up, the SDK runs on the cached configuration and falls back quickly; repeated session attempts are throttled. ## Test mode [Section titled “Test mode”](#test-mode) With `testMode = true` the adapter replaces every unit with Google’s public test units and Qartvelo Ads serves only non-billable test creatives. Add `testForceNoFill = true` to see the fallback every time. Never ship a release with test mode or Google’s test ids. ## Consent [Section titled “Consent”](#consent) The adapter does not show consent forms and does not set consent. It respects your Google UMP / CMP configuration and only adds the restrictions you pass through `QartveloAds.setPrivacy`. See [Privacy](/guides/privacy/).
# Custom fallback adapter
> Plug another ad network into the fallback seam with the FallbackAdapter interface.
The AdMob adapter is one implementation of a small interface in `qartvelo-ads-core`. You can implement it yourself to fall back to another network, or to your own house ads.
```kotlin
QartveloAds.registerFallbackAdapter(MyNetworkAdapter())
```
Register before or after `initialize`; a registered adapter replaces the auto-discovered AdMob adapter. The fallback rules are unchanged: the placement’s fallback provider must not be `none`, `admobFallback` must be `true`, and an ad unit id must be known (`admobAdUnits` or the dashboard), or test mode must be on. The id string is passed to your adapter as-is, so you can map placement codes to your own network’s ids through `admobAdUnits`. ## Interface [Section titled “Interface”](#interface) Package `com.qartvelo.sdk.fallback`:
```kotlin
interface FallbackAdapter {
val networkName: String // for logs, e.g. "mynetwork"
fun initialize(context: Context, settings: FallbackSettings) // once; cheap; never throw
fun updateSettings(settings: FallbackSettings) // test mode or privacy changed
fun loadInterstitial(context: Context, placementId: String, adUnitId: String, callback: FallbackLoadCallback)
fun isInterstitialReady(placementId: String): Boolean
fun showInterstitial(activity: Activity, placementId: String, callback: FallbackShowCallback)
fun loadRewarded(context: Context, placementId: String, adUnitId: String, callback: FallbackLoadCallback)
fun isRewardedReady(placementId: String): Boolean
fun showRewarded(activity: Activity, placementId: String, callback: FallbackShowCallback)
fun createBanner(context: Context, placementId: String, adUnitId: String, widthDp: Int,
callback: FallbackBannerCallback): FallbackBanner
}
data class FallbackSettings(val testMode: Boolean, val privacy: QartveloAdsPrivacy)
interface FallbackLoadCallback { fun onLoaded(); fun onFailed(message: String) }
interface FallbackShowCallback {
fun onShown() {}
fun onImpression() {}
fun onClicked() {}
fun onReward(type: String, amount: Int) {} // only after the network confirmed the reward
fun onDismissed() {}
fun onShowFailed(message: String) {}
}
interface FallbackBannerCallback {
fun onLoaded(); fun onFailed(message: String)
fun onImpression() {}; fun onClicked() {}
}
interface FallbackBanner {
val view: View
fun pause(); fun resume(); fun destroy()
}
```
## Contract [Section titled “Contract”](#contract) * **Threading**: core calls every method on the main thread. Callbacks may be invoked from any thread; core marshals them to the main thread and de-duplicates terminal events. * **Keys**: ads are keyed by Qartvelo Ads placement id, so two placements may share one network unit. * **Test mode**: when `settings.testMode` is true, substitute the network’s public test units. An empty `adUnitId` is only ever passed in test mode. * **Rewards**: call `onReward` only after the network confirmed the reward; core guarantees the app sees it at most once. * **Banners**: `createBanner` receives a `MutableContextWrapper` owned by core, which swaps its base context between Activities so the banner never leaks one. Start loading immediately and report through the callback; core calls `pause`, `resume` and `destroy`. * **Never throw**: the SDK guards calls, but a throwing adapter turns every fallback into a failure. * **R8**: core’s consumer rules keep the `com.qartvelo.sdk.fallback` interfaces; keep your adapter class if you minify and load it by name. The AdMob implementation, `com.qartvelo.admob.AdMobFallbackAdapter` in the [SDK repository](https://github.com/Qartvelo-com/ads/tree/main/android/qartvelo-ads-admob), is a complete reference.
# Privacy
> What the SDK collects and never collects, consent signals, retention, and Google Play Data safety.
Qartvelo Ads targets ads by **context**, not by person. The SDK and backend are built so that no cross-app profile of a user can exist. ## Contextual targeting only [Section titled “Contextual targeting only”](#contextual-targeting-only) Campaigns can target country (derived on the server from the request’s network address), app, app category, ad format, content language and Android major version. There is no behavioural, interest or audience targeting, and no retargeting. ## Data the SDK sends [Section titled “Data the SDK sends”](#data-the-sdk-sends) | Request | Fields | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/sdk/initialize` | app key, package name, SDK version, app version, platform, OS version, test flag | | `POST /api/v1/ads/request` | app key, placement code, format, session token, device language, Android version, app and SDK version, screen width and height in pixels, test flags | | `POST /api/v1/events/impression`, `click`, `reward` | request id, signed impression token, reward completion flag | | `POST /api/v1/events/fallback` | session token, placement code, fallback reason | The HTTP user agent contains the SDK version, Android version and package name. ## Data the SDK never collects [Section titled “Data the SDK never collects”](#data-the-sdk-never-collects) * No Advertising ID (GAID), Android ID, IMEI, serial number or any other device identifier. * No precise or coarse location from the device; no location permission is requested. * No contacts, accounts, installed-app lists, call logs, SMS, photos or files. * No names, emails, phone numbers or other personal details. * No persistent user identifier: nothing is stored that could recognize the same person across apps or sessions. ## Ephemeral session [Section titled “Ephemeral session”](#ephemeral-session) At start-up the backend issues a short-lived signed session token (one hour by default) carrying a **random** session id, the app id, package name, timestamps and the test flag. The token is kept in memory only, never written to disk and never logged; a new one is created on the next start or when it expires. Frequency caps use this session id, so they reset with the session by design. On the device the SDK stores only the last remote placement configuration (no user data) in a private SharedPreferences file, and downloaded creatives in the app’s cache directory, deleted once their ads expire. ## Server side [Section titled “Server side”](#server-side) * The IP address is used transiently to infer the country and is never stored. Security logs keep only a salted hash of the client network where needed for fraud checks. * Impression tokens are stored as SHA-256 hashes only. * Long-term reporting uses aggregated daily statistics per app, placement and campaign. ## Consent API [Section titled “Consent API”](#consent-api)
```kotlin
QartveloAds.setPrivacy(
QartveloAdsPrivacy(
consentGiven = true, // result of YOUR consent flow; null = unknown
childDirected = false, // app or request treated as child-directed; null = unknown
underAgeOfConsent = null, // user under the age of consent; null = unknown
),
)
```
```ts
QartveloAds.setPrivacy({ consentGiven: true, childDirected: false });
```
* Every field defaults to unknown. The SDK never assumes or claims consent and never shows consent UI. * Call it any time, before or after `initialize`; new values are forwarded to the fallback adapter immediately. * Qartvelo Ads serves contextual ads that do not depend on consent for personalization; the signals mainly govern the AdMob fallback. ## AdMob fallback and consent [Section titled “AdMob fallback and consent”](#admob-fallback-and-consent) Google’s SDK is subject to Google’s policies and your agreement with Google. Collect consent where required (for example with Google’s User Messaging Platform or a certified CMP) before ads are requested. The adapter respects your Google Mobile Ads configuration and only adds restrictions: * `childDirected = true` sets Google’s age-restricted treatment to `CHILD`; `underAgeOfConsent = true` sets it to `TEEN`. `false` or `null` leaves your own `RequestConfiguration` untouched, and a stricter value you set yourself is never relaxed. * `consentGiven = false` requests non-personalized AdMob ads (`npa=1`). * The adapter never writes TCF/UMP consent strings and never grants consent. ## Retention [Section titled “Retention”](#retention) * Raw security logs are deleted after the retention period (default 30 days). * Impressions, clicks and reward events are kept for billing, payouts and fraud review, but the per-event session hash and country are removed after the retention period. * On the device, the cached configuration is replaced on every successful start; creative files are removed when their ads expire (at most 30 minutes) or on the next start. Uninstalling the app removes everything. ## Google Play Data safety [Section titled “Google Play Data safety”](#google-play-data-safety) Use the tables above when filling in your Data safety form. The Qartvelo Ads SDK itself does not collect personal information, device identifiers or location from the device, and all traffic is encrypted in transit (HTTPS). Ad interaction events (impressions, clicks, reward completions) are sent to serve and bill ads and to prevent fraud. If you use the AdMob fallback, also include Google Mobile Ads’ disclosures, which Google publishes for its SDK. You are responsible for your app’s declarations.
# Account and apps
> Create a publisher account, register your Android app and manage its app key.
## Create a publisher account [Section titled “Create a publisher account”](#create-a-publisher-account) 1. Go to [ads.qartvelo.com/register](https://ads.qartvelo.com/register). 2. Choose **Publisher**, enter your name, company name, email and a password. 3. Confirm your email address. The dashboard opens after verification. New publisher accounts start as **pending** until a Qartvelo Ads admin approves them. While pending you can already register apps, create placements and test with [test mode](/get-started/test-mode/); live ads are served only once the account and the app are approved. | Account status | What it means | | -------------- | -------------------------------------------------------------------------------- | | pending | Waiting for review. Apps and placements can be managed; only test ads are served | | approved | Live ads can be served to approved apps | | suspended | Serving is stopped; apps and placements are read-only | | rejected | The account was not accepted | ## Register an app [Section titled “Register an app”](#register-an-app) Under **Apps**, choose **Add app** and fill in: | Field | Rules | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Name | Shown in your dashboard and to advertisers who target specific apps | | Package name | Your Android `applicationId`, for example `com.example.game`. Must be unique on Qartvelo Ads. **Locked after the app is reviewed**, because the SDK validates it on every start | | Category | One of: games, news, entertainment, education, lifestyle, sports, finance, shopping, social, tools, travel, health, music, other. Advertisers can target categories | | Default language | `ka`, `en` or `ru`. Used for language targeting when the device does not report a supported language | Each new app is **pending** until an admin approves it. Apps can also be **rejected** or **suspended**. Package name must match exactly The backend compares the package name sent by the SDK with the registered one. If your debug build uses an `applicationIdSuffix` such as `.debug`, it is a different package: initialization fails with `package_mismatch`. Register the suffix-less id and test with the same package, or remove the suffix for ad testing. ## Credentials [Section titled “Credentials”](#credentials) The app page shows two credentials: | Credential | Where it goes | Secret? | | ---------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **App key** `app_` + 24 characters | In your app code (`QartveloAds.initialize`) | No. It is public by design and is only accepted together with the registered package name | | **SDK secret** | Nowhere in the app. Store it on your server | Yes. Shown once when created; generate a new one if lost. Reserved for server-to-server features | Never embed the SDK secret in an APK or JavaScript bundle. ### Rotating credentials [Section titled “Rotating credentials”](#rotating-credentials) * **Generate new app key**: builds that still use the old key stop receiving Qartvelo Ads ads and fall back to AdMob until you ship an update with the new key. Only rotate a key that was abused. * **Generate new SDK secret**: the old secret stops working immediately; the new one is shown once. ## The Integration panel [Section titled “The Integration panel”](#the-integration-panel) The app page has an **Integration** panel with Kotlin and React Native snippets already filled in with your app key, the API URL and your placement codes. Copy them as a starting point; the rest of this documentation explains every option. ## Deleting an app [Section titled “Deleting an app”](#deleting-an-app) Apps without traffic history can be deleted from the app page. Apps that have served traffic are kept for billing and reporting.
# Placements
> Configure ad slots, fallback, timeouts, frequency caps and banner refresh from the dashboard.
A placement is one ad slot in your app. Your code addresses it only by its **code**; everything else is remote configuration you can change at any time without an app update. ## Fields [Section titled “Fields”](#fields) | Field | Values | Default | Notes | | -------------------- | ------------------------------------------------ | ------- | --------------------------------------------------------------------------------------- | | Name | text | | For your reports | | Code | `[a-z0-9_]{2,64}` | | Unique per app, for example `home_banner`. Used in code: `loadInterstitial("game_end")` | | Format | `banner`, `interstitial`, `rewarded` | | Cannot change after the placement received traffic; create a new placement instead | | Qartvelo Ads enabled | on / off | on | Off sends every request straight to the fallback | | Fallback provider | `admob`, `none` | `admob` | What the SDK shows when Qartvelo Ads cannot fill | | AdMob ad unit ID | `ca-app-pub-/` | | Required when the fallback is AdMob. Your own unit, of the same format | | Request timeout | 100 to 5000 ms | 800 | Qartvelo Ads time budget before falling back. Wins over the SDK option | | Frequency cap | count (1 to 1000) per `session`, `hour` or `day` | none | Maximum Qartvelo Ads impressions of this placement per session id | | Banner refresh | 30 to 3600 s | 60 | Banners only | | Status | `active`, `paused` | active | Paused placements are not served by Qartvelo Ads (fallback still applies) | ## Naming placement codes [Section titled “Naming placement codes”](#naming-placement-codes) * Name by location and moment, not by network: `level_complete`, `settings_banner`, `revive_reward`. * Use one code per simultaneously visible banner. Two banners on screen at once need two banner placements. * Keep codes stable. Reports are per placement, so renaming a code in code without a matching placement leads to `placement_not_found` / `INVALID_PLACEMENT`. ## Overriding AdMob units in code [Section titled “Overriding AdMob units in code”](#overriding-admob-units-in-code) The SDK option `admobAdUnits` maps placement codes to AdMob unit ids and wins over the dashboard value. Use it if you prefer to keep unit ids in your build configuration:
```kotlin
QartveloAdsOptions(
admobAdUnits = mapOf(
"home_banner" to "ca-app-pub-XXX/111",
"game_end" to "ca-app-pub-XXX/222",
),
)
```
## Admin controls [Section titled “Admin controls”](#admin-controls) Qartvelo Ads admins can also disable a placement, an app or a publisher. A placement disabled by an admin behaves like a paused placement and cannot be re-enabled from your dashboard. The SDK re-checks a disabled placement at most every 5 minutes, so re-enabling reaches running apps without a restart. ## How the effective timeout is chosen [Section titled “How the effective timeout is chosen”](#how-the-effective-timeout-is-chosen) 1. The placement’s request timeout from the dashboard, if the SDK has a config for it. 2. Otherwise `QartveloAdsOptions.requestTimeoutMs` (default 800). 3. The SDK clamps the result to 100..10000 ms. The timeout covers the Qartvelo Ads session and ad request only. Creative downloads have their own limits (10 s for images, 45 s for video), and users never wait for either: loads are asynchronous and AdMob preloads in parallel.
# Reports and payouts
> Understand your traffic metrics, revenue share and how earnings are paid out.
## Metrics [Section titled “Metrics”](#metrics) The publisher dashboard reports per day, app and placement. Days follow the `Asia/Tbilisi` timezone. | Metric | Meaning | | ----------- | ------------------------------------------------------------------------------------------- | | Requests | Qartvelo Ads ad requests from the SDK | | Matches | Requests that returned an ad (`fill`) | | No fills | Requests that returned `no_fill` | | Fill rate | Matches / requests | | Impressions | Accepted impression events (billed) | | Clicks | Accepted clicks (at most one per impression) | | CTR | Clicks / impressions | | Revenue | Your share of the advertiser spend on your impressions | | Fallbacks | Times the SDK handed a request to your fallback (no fill, timeout, error, creative failure) | Requests, matches, no fills and fallbacks can lag up to one minute; impressions, clicks and revenue are updated in real time. Test traffic is never counted. Reports can be exported as CSV. AdMob impressions and revenue are **not** in Qartvelo Ads reports: they belong to your AdMob account and appear in AdMob. Qartvelo Ads only counts how often a fallback happened. ## Revenue share [Section titled “Revenue share”](#revenue-share) You earn a percentage of what advertisers pay for impressions in your apps. The platform default is **70%**; an individual agreement can set a different share for your account. All amounts are in **GEL**. Revenue per impression = campaign CPM bid / 1000 x your revenue share. ## Balances and payouts [Section titled “Balances and payouts”](#balances-and-payouts) | Balance | Meaning | | -------- | -------------------------------------------------- | | Pending | Earned from impressions, not yet reviewed | | Approved | Reviewed by Qartvelo Ads and scheduled for payment | | Paid | Paid out to you | Payouts are manual: Qartvelo Ads reviews earnings (including fraud review), approves an amount, which creates a payout in the **Payouts** page, and marks it paid once transferred. Contact Qartvelo Ads for payment details and schedule. ## Invalid traffic [Section titled “Invalid traffic”](#invalid-traffic) Impressions and clicks that fail validation are never billed and never earn revenue. This includes duplicates and replays, events after the ad expired, clicks less than a second after the impression, implausibly high click-through rates and unusually high request or impression rates from one session. See [fraud rules](/api/errors-and-limits/#fraud-rules). Never click your own live ads: use [test mode](/get-started/test-mode/).
# FAQ
> Frequently asked questions about Qartvelo Ads for publishers and developers.
### Do I have to remove AdMob? [Section titled “Do I have to remove AdMob?”](#do-i-have-to-remove-admob) No. Keep your AdMob account and ad units; Qartvelo Ads uses them as the fallback for every request it cannot fill. AdMob revenue keeps going directly to you. ### Does the SDK slow down my app? [Section titled “Does the SDK slow down my app?”](#does-the-sdk-slow-down-my-app) No network or disk work happens on your calling thread, loads are asynchronous, the Qartvelo Ads request has an 800 ms budget by default, and AdMob preloads in parallel. Start-up does not wait for the backend when a cached configuration exists. ### Does Qartvelo Ads support iOS? [Section titled “Does Qartvelo Ads support iOS?”](#does-qartvelo-ads-support-ios) Not yet. The React Native API is platform-neutral and rejects with `unsupported_platform` on iOS, so shared code keeps working. Use `QartveloAds.isSupported()` to hide ad UI on iOS. ### Is the app key a secret? [Section titled “Is the app key a secret?”](#is-the-app-key-a-secret) No. It is public by design and is only accepted together with your registered package name. The SDK secret is the secret; never put it in an app. ### Can I test before my app is approved? [Section titled “Can I test before my app is approved?”](#can-i-test-before-my-app-is-approved) Yes. Test mode works for pending apps and pending accounts. ### Can I use one placement code for two banners? [Section titled “Can I use one placement code for two banners?”](#can-i-use-one-placement-code-for-two-banners) Only one banner per placement code can be visible at a time. Create one placement per simultaneously visible banner. ### How is my revenue calculated? [Section titled “How is my revenue calculated?”](#how-is-my-revenue-calculated) Each Qartvelo Ads impression earns the campaign’s CPM bid / 1000 times your revenue share (70% by default), in GEL. See [Reports and payouts](/publishers/reports-and-payouts/). ### What does the SDK collect? [Section titled “What does the SDK collect?”](#what-does-the-sdk-collect) No advertising ID, no device identifiers, no location, no personal data. See [Privacy](/guides/privacy/). ### Do I need a consent dialog for Qartvelo Ads? [Section titled “Do I need a consent dialog for Qartvelo Ads?”](#do-i-need-a-consent-dialog-for-qartvelo-ads) Qartvelo Ads ads are contextual and do not use personal data for personalization. Google’s AdMob, used as fallback, has its own consent requirements; collect consent with your CMP where required and pass the result with `setPrivacy`. ### Can I use Qartvelo Ads with another network than AdMob? [Section titled “Can I use Qartvelo Ads with another network than AdMob?”](#can-i-use-qartvelo-ads-with-another-network-than-admob) Yes, by implementing the [fallback adapter interface](/guides/custom-fallback-adapter/). ### Which Android versions are supported? [Section titled “Which Android versions are supported?”](#which-android-versions-are-supported) Android 6.0 (API 23) and newer for native apps; React Native requires API 24. ### Where is the source code? [Section titled “Where is the source code?”](#where-is-the-source-code) [github.com/Qartvelo-com/ads](https://github.com/Qartvelo-com/ads), MIT licensed. It includes a native sample app and a React Native example.
# Glossary
> Terms used across the Qartvelo Ads documentation and dashboards.
| Term | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------- | | **Advertiser** | A company that buys impressions through campaigns | | **App key** | Public identifier of a registered app (`app_` + 24 characters), passed to `initialize` | | **CPM** | Cost per mille: the price for 1000 impressions. One impression costs CPM / 1000 | | **Campaign** | An advertiser’s budget, bid, schedule and targeting, with one or more creatives | | **Creative** | The image or video shown as an ad, with a destination URL | | **CTR** | Click-through rate: clicks / impressions | | **Fallback** | Showing the publisher’s own AdMob (or custom) ad when Qartvelo Ads cannot serve | | **Fill / no fill** | Whether an ad request returned an ad | | **Fill rate** | Fills / requests | | **Frequency cap** | Maximum impressions per session, hour or day for one viewer session | | **Impression** | One ad actually displayed; the billing event | | **Impression token** | Signed, single-use token that authorizes the events of one ad | | **Kill switch** | A setting that turns Qartvelo Ads serving off globally or for a publisher, app or placement | | **Pacing** | Spreading a daily budget evenly over the day | | **Placement** | One ad slot in an app, identified by its code and with one format | | **Placement code** | The string your code uses for a placement, for example `game_end` | | **Publisher** | A company that shows ads in its apps | | **Remote configuration** | Placement settings delivered by `/sdk/initialize` and cached by the SDK | | **Revenue share** | The publisher’s percentage of advertiser spend (70% by default) | | **SDK secret** | Server-side credential of an app. Never embedded in apps | | **Session** | A random, one-hour id issued at start-up; never linked to a person or device | | **Test mode** | SDK option that serves non-billable test creatives and Google test units |
# Troubleshooting
> Diagnose initialization failures, missing ads, fallback problems and build errors.
Start with debug logging: `logLevel = QartveloAdsLogLevel.DEBUG` (`logLevel: 'debug'` in React Native), then `adb logcat -s QartveloAds`. Logs show request timing, fallback decisions and event delivery (never tokens). ## Initialization fails [Section titled “Initialization fails”](#initialization-fails) | Symptom | Cause and fix | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `NOT_INITIALIZED` with `invalid_app_key` in the log | Wrong app key. Copy it from the app page in the dashboard | | `package_mismatch` | The running `applicationId` differs from the registered package name. Check `applicationIdSuffix` in debug builds and flavors | | `app_not_approved` | The app is still pending. Use test mode until it is approved | | `NETWORK_ERROR` / `TIMEOUT` | No connectivity, a custom `baseUrl` that is wrong, or cleartext HTTP blocked for a local backend | | The listener never reports a second result | `initialize` is idempotent; only the first call’s options count. Restart the process | Initialization failure is not fatal: the SDK keeps running on its cached configuration and can fall back to AdMob. ## No ads appear [Section titled “No ads appear”](#no-ads-appear) 1. Is the placement code exactly the one in the dashboard, and the format right? A wrong format gives `INVALID_PLACEMENT` (`format_mismatch` / `placement_not_found` in the log). 2. In test mode you should always get a **TEST AD** creative unless `testForceNoFill` is on. If test mode works but live does not, check that the account, app and placement are approved and active. 3. Live no-fill is normal when no campaign targets your app’s category, language or users’ Android version. Configure an AdMob fallback so the slot is still filled. 4. `onFallbackStarted` with reason `disabled` means Qartvelo Ads is switched off for the placement, app or account. 5. For full-screen ads, make sure you call `show*` only after `onLoaded`, from a resumed Activity, and that no other full-screen ad is on screen (`ALREADY_SHOWING`). ## Fallback does not show AdMob [Section titled “Fallback does not show AdMob”](#fallback-does-not-show-admob) | Check | | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Adapter included | `com.qartvelo.ads:admob` (Android) or `QartveloAds_admobEnabled=true` (React Native) | | Option | `admobFallback` is not `false` | | Placement | Fallback provider is **AdMob**, not **None** | | Unit id | Set on the placement or in `admobAdUnits`, and of the same format as the placement. An App ID (with `~`) is not an ad unit id | | AdMob side | New AdMob units can take hours to serve; AdMob may also have no fill. Test with `testMode` (Google test units) first | ## Build errors [Section titled “Build errors”](#build-errors) | Error | Fix | | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `Could not find com.qartvelo.ads:core:0.3.0` | Add `maven("https://jitpack.io")` to the repositories used for dependencies (`dependencyResolutionManagement` or `allprojects`) | | `AAPT: error: attribute qartvelo_placementId not found` | Declare `xmlns:app="http://schemas.android.com/apk/res-auto"` and make sure `com.qartvelo.ads:core` is a dependency of that module | | `attribute ourads_placementId not found` | Old attribute name from pre-release snippets. Use `app:qartvelo_placementId` | | `Unresolved reference: OURADS` | The enum value is `AdSource.QARTVELO` | | Crash at start: “The Google Mobile Ads SDK was initialized incorrectly” | The AdMob adapter is present but the AdMob App ID meta-data is missing from the manifest | | Duplicate class / version conflicts with `play-services-ads` | The adapter depends on 25.4.0 normally; your newer version wins. Align with Gradle’s dependency resolution if you pin an older one | ## Events and reporting [Section titled “Events and reporting”](#events-and-reporting) * Impressions are counted only when the ad is actually on screen; loads alone count as requests. * Requests, no-fills and fallbacks in reports can lag up to a minute; impressions and revenue are real time. * Test traffic never appears in reports. * Clicking your own ads is detected (fast clicks, high CTR) and not counted. Still stuck? Open an issue at [github.com/Qartvelo-com/ads/issues](https://github.com/Qartvelo-com/ads/issues) with the SDK version, a DEBUG log and the placement code (never your SDK secret).
# Changelog
> Release history of the Qartvelo Ads SDKs.
Releases are tagged in [Qartvelo-com/ads](https://github.com/Qartvelo-com/ads). The Android SDK, the AdMob adapter and the React Native plugin share one version number. ## 0.3.0 [Section titled “0.3.0”](#030) * Short Maven coordinates: `com.qartvelo.ads:core` and `com.qartvelo.ads:admob`, identical on JitPack, GitHub Packages and `mavenLocal()`. * Repository moved to `Qartvelo-com/ads`. * The React Native plugin depends on `com.qartvelo.ads:core`; the group-switch Gradle property was removed. `QartveloAds_sdkVersion` still pins the native version. * Native sample renamed to `com.qartvelo.sample`. ## 0.2.0 [Section titled “0.2.0”](#020) First public release. * Android core SDK (`com.qartvelo.sdk`): banner, interstitial and rewarded ads, creative caching, remote configuration with offline cache, ordered event delivery with retries. * Optional AdMob fallback adapter with parallel preloading and automatic discovery. * React Native plugin `@qartvelo/react-native-ads`: TurboModule and Fabric banner on the New Architecture. * Native sample app and React Native example app.
# Billing
> Prepaid balance, what you are charged for, and currency.
* **Prepaid.** Campaigns spend from your account balance. Qartvelo Ads tops up the balance after you pay; contact Qartvelo Ads for an invoice or bank transfer details. Every top-up and adjustment appears in **Billing** with its date and note. * **Currency.** All prices, budgets and balances are in Georgian lari (GEL). * **Charged per impression.** One impression costs your CPM bid / 1000, charged when the ad was actually displayed and the impression passed validation. Clicks are free. * **No overspend.** A campaign never spends more than its total budget, its daily budget or your balance; budgets are reserved per impression atomically. * **Invalid traffic is free.** Duplicates, replays, expired ads and events rejected by fraud rules are never charged. Test traffic is never charged. * **Balance runs out.** When your balance cannot pay for another impression, your campaigns stop serving until it is topped up. Top-ups reach the ad server within about a minute.
# Campaigns
> Create CPM campaigns, submit them for review and manage their lifecycle.
Advertisers buy impressions in Georgian Android apps with CPM campaigns: you set a price per 1000 impressions and a budget, upload creatives, choose contextual targeting, and pay only for delivered impressions from a prepaid balance. ## Get started [Section titled “Get started”](#get-started) 1. Register at [ads.qartvelo.com/register](https://ads.qartvelo.com/register) as an **Advertiser** with your company name, and verify your email. 2. Your account starts as **pending**. You can already build campaigns; they serve once your account is approved and your balance is topped up (see [Billing](/advertisers/billing/)). 3. Create a campaign, add at least one creative, then **Submit for review**. ## Campaign settings [Section titled “Campaign settings”](#campaign-settings) | Field | Rules | | ------------- | ------------------------------------------------------------------------------------------------ | | Name | Up to 255 characters | | CPM bid | 0.01 to 10000 GEL per 1000 impressions, up to 4 decimals. One impression costs bid / 1000 | | Total budget | At least 1 GEL and enough for one impression | | Daily budget | Optional; at least 1 GEL and not above the total budget. Delivery is paced evenly across the day | | Start / end | Optional, in Georgian time (`Asia/Tbilisi`). End must be in the future and after the start | | Frequency cap | Optional: 1 to 1000 impressions per `session`, `hour` or `day` for one viewer session | | Targeting | See [Targeting](/advertisers/targeting/) | Only CPM pricing is available. Clicks are tracked and reported but not charged. ## Statuses [Section titled “Statuses”](#statuses)
```plaintext
draft ──submit──> pending_review ──approve──> active (start reached)
^ │ └──> approved (scheduled; becomes active at start)
└── edit ── rejected <┘ reject
active ⇄ paused active / approved / paused ──> completed
```
| Status | Meaning | | ---------------- | -------------------------------------------------------------------------------------------- | | `draft` | Being built. Editable | | `pending_review` | Submitted; Qartvelo Ads reviews settings and creatives | | `approved` | Approved, waiting for its start time | | `active` | Serving. Only active campaigns are served | | `paused` | Stopped by you or by an admin. A campaign paused by an admin can only be resumed by an admin | | `rejected` | Not accepted; the reason is shown. Edit and resubmit | | `completed` | Ended at its end date, when the total budget is spent, or completed manually. Final | * Submitting requires at least one creative that is pending review or approved, and an end date that has not passed. * Settings and targeting can be edited only in `draft` and `rejected`. To change a running campaign, complete it and create a new one. * Resuming requires an end date in the future and remaining budget. ## How delivery works [Section titled “How delivery works”](#how-delivery-works) For every ad request, Qartvelo Ads considers active campaigns that match the request’s context and have budget, then picks the **highest CPM bid** (ties are random). A daily budget is paced: the campaign is skipped while its spend today is ahead of an even pace (with 10% slack). Budgets are reserved atomically per impression, so a campaign can never overspend its total budget, its daily budget or your balance. ## Reports [Section titled “Reports”](#reports) The **Reports** page shows impressions, clicks, CTR, spend and effective CPM per day, campaign, creative, app and placement, and exports to CSV. Spend is updated in real time. Invalid traffic (duplicates, replays, implausible click rates) is rejected before billing and never charged.
# Creative specifications
> Allowed formats, sizes, file types and limits for banner, interstitial and rewarded creatives.
Upload creatives on the campaign page. Each creative is reviewed separately and serves only once **approved**; pending and rejected creatives are never shown. Approved creatives are locked; to change one, upload a new creative. ## Formats [Section titled “Formats”](#formats) | Format | Creative type | Where it appears | | ------------ | -------------- | ------------------------------------------------------------------ | | Banner | Image | A strip inside the app’s UI, refreshed every 30 s or more | | Interstitial | Image or video | Full screen at a natural break; the user can close it | | Rewarded | Video | Full screen, opted in by the user in exchange for an in-app reward | ## Images [Section titled “Images”](#images) | | | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | File types | PNG, JPEG, WebP, GIF | | Maximum size | 1 MB | | Maximum dimension | 4096 px | | Banner sizes | Exactly one of 320x50, 320x100, 300x250, 468x60, 728x90 | | Interstitial size | Short side at least 320 px and long side at least 480 px. Use 1080x1920 (portrait) or 1920x1080 (landscape); the ad server prefers creatives matching the device orientation | Supply several banner sizes in one campaign: the ad server picks a size that fits the device’s screen width. ## Video [Section titled “Video”](#video) | | | | ----------------- | ------------------------------------------------------------------ | | Container / codec | MP4, H.264 | | Maximum size | 30 MB | | Duration | 5 to 60 seconds | | Recommended | 720x1280 or 1080x1920 portrait, AAC audio, 15 to 30 s, `faststart` | Videos are downloaded completely before they are shown, so a smaller file loads on more devices in time. Rewarded videos must be watched to the end to grant the reward. ## Destination URL [Section titled “Destination URL”](#destination-url) Every creative needs a landing page URL starting with `https://`. It opens in the user’s browser when they tap the ad. Make sure it works on mobile. ## Review guidelines [Section titled “Review guidelines”](#review-guidelines) Qartvelo Ads reviews every creative and its landing page before it can serve. If a creative is rejected, the reason is shown on it; fix the issue and upload a corrected creative. Pending or rejected creatives can still have their destination URL edited. ## Formats targeted by the campaign [Section titled “Formats targeted by the campaign”](#formats-targeted-by-the-campaign) If a campaign targets specific formats, you can only upload creatives of those formats. A campaign serves a format only when it has at least one approved creative of that format.
# Targeting
> Contextual targeting options for campaigns. No personal data or user profiles.
Qartvelo Ads targeting is **contextual only**: it uses the app and the request, never a person’s history. There are no advertising IDs, audiences or retargeting. Every option is optional; empty means “any”. | Option | Values | Matched against | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | Country | ISO 3166-1 alpha-2 code, for example `GE` | Derived from the network address at the edge. Requests without a known country count as Georgia | | Languages | `ka`, `en`, `ru` (several) | The device language, else the app’s default language | | Android versions | `6` to `16` (several) | The device’s Android major version. Requests without a version do not match a campaign that targets versions | | App categories | games, news, entertainment, education, lifestyle, sports, finance, shopping, social, tools, travel, health, music, other | The publisher’s app category | | Apps | Up to 500 approved apps | Specific apps | | Formats | banner, interstitial, rewarded | The placement’s format | | Frequency cap | 1 to 1000 per session, hour or day | Impressions of this campaign within one viewer session | A viewer session is a random id that lasts about an hour and is never linked to a person, so frequency caps are approximate by design. ## Tips [Section titled “Tips”](#tips) * Start broad and narrow down using the per-app and per-placement breakdown in Reports. * Language targeting is the best proxy for the creative’s language: target `ka` for Georgian-language creatives. * Rewarded video usually has the highest attention; banners the lowest price.