Adfacten Developers

Mobile SDK: show Adfacten ads in your app

The Adfacten mobile SDK shows ads in Android and iOS apps. It requests the ad, renders it, measures impressions and clicks, and for rewarded ads has Adfacten notify your server of the reward. This page is for publishers (app developers). Advertisers who want to measure app installs should read MMP integration.

Türkçe sürüm: Mobil SDK.

Source code and samples: Android (github.com/adfacten/adfacten-android-sdk), iOS (github.com/adfacten/adfacten-ios-sdk), Flutter (github.com/adfacten/adfacten_ads_flutter), React Native (github.com/adfacten/adfacten-react-native-ads) and Unity (github.com/adfacten/adfacten-unity-ads). The panel also shows a ready-made code sample for every placement.

On this page:

  1. Overview: formats and billing
  2. Getting started in the panel: app, app-ads.txt, approval, going live
  3. Android
  4. iOS
  5. Flutter, React Native and Unity
  6. Rewarded ads: server-side verification
  7. Privacy and consent (TCF, GPP, COPPA, KVKK)
  8. Direct REST (custom integrations / mediation)
  9. Troubleshooting

1. Overview

Format When to use it
Banner A persistent ad in part of the screen. Sizes: 320x50, 320x100, 300x250, 728x90 (tablets only). Refreshes every 30–120 s while visible (placement setting; 0 = no refresh) and pauses when hidden.
Interstitial Full screen, at natural breaks (end of a level, screen transitions).
Rewarded The user watches the ad to the end and earns a reward (e.g. 10 coins). The reward type and amount are set on the placement.
Native The ad's assets (title, text, image, icon, button) are laid out in your own design.
App open A full-screen ad shown at launch or when the app returns from the background.

How billing is counted

The SDK sends each event once. Offline, it queues events and retries them for up to 24 hours.

2. Getting started in the panel

2.1 Add your app

Panel → Apps → Add app.

Then add placements to the app (one per ad slot). Each placement has an id (plc_…) and exactly one format. For banners you choose the size(s) and the refresh interval; for rewarded placements the reward type and amount. You can also set a daily per-device impression cap.

2.2 Child-directed apps are not supported

Adfacten ads are not served in child-directed apps (US COPPA and the Google Play Families policy). An app marked child-directed cannot be added; if it is marked later, every ad request is refused (coppa). Only the network team can remove that flag.

2.3 Test mode and test ads

New apps start in test mode. Test ads behave like real ones (render, viewability and click events work; the click goes to https://adfacten.com/) but are never billed and are labelled "Test ad".

The server returns test ads when any of these is true:

Condition How to clear it
The app is pending review Network approval (2.5)
The app-ads.txt line was not found Add the line (2.4)
The app's test mode is on Switch it off in the panel (2.6)
testMode is on in the SDK (test=true in the request) Release builds must have testMode off
The device is an emulator / simulator Test on a real device

Test creatives are fixed: banner = image of the requested size, interstitial = HTML, rewarded = 5-second video with reward, native = sample assets, app open = 320x480 image.

2.4 The app-ads.txt line

app-ads.txt proves that the app is yours and that Adfacten is authorised to sell its inventory (IAB app-ads.txt standard). Real ads serve once the line is verified.

Where: at the root of the developer website listed on the app's store page: https://<developer domain>/app-ads.txt. A leading www., m. or mobile. is ignored (https://www.company.com → https://company.com/app-ads.txt). HTTPS is tried first, then HTTP.

The line (shown ready to copy on your app's page in the panel):

adfacten.com, <your publisher id>, DIRECT

Verification: the file is checked as soon as you add the app, then re-checked about every 6 hours. In the panel, Check again runs a check at most once a minute.

Status Meaning
found The line is there. Real ads can serve (if approved and test mode is off).
wrong_id An adfacten.com line exists, but with someone else's publisher id (copied from elsewhere).
no_line The file exists but has no adfacten.com line.
missing No app-ads.txt on the website (404, or an HTML page instead of the file).
no_website The store page lists no developer website. Add it in the store console.
unreachable The website did not answer. Retried later; the last good result is kept.
unknown Not checked yet (e.g. the app was not found in the store).

If you changed the developer website in the store listing, the store may take a while to publish it. Then press Check again.

2.5 Network approval

Your app goes into the Adfacten network team's review queue. Approval requires the app to be live in the store. You are notified when it is approved or rejected; the rejection reason is shown on the app's page. Rejected apps receive no ads (app_not_approved).

2.6 Going live

Real ads serve only when all four hold:

  1. the app is approved,
  2. the app-ads.txt status is found,
  3. the app's test mode is off in the panel,
  4. the SDK's testMode is off in your release build.

The Serving status on the app's page in the panel shows what is missing (live, test or blocked, with the reason).

2.7 sellers.json

Adfacten's list of authorised sellers: https://adfacten.com/sellers.json (IAB sellers.json). Publishers with at least one approved app are listed. Companies appear with their name and domain; individuals are listed as confidential (is_confidential) under Turkish data-protection law (KVKK), so your name is not published.

3. Android

Requirements: Android 5.0 (API 21) or later. Package: com.adfacten.ads. Source code and sample app: https://github.com/adfacten/adfacten-android-sdk (Apache 2.0).

3.1 Installation

settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://cdn.adfacten.com/maven/") {
            content { includeGroup("com.adfacten") }
        }
    }
}

The app module's build.gradle.kts:

dependencies {
    implementation("com.adfacten:ads:1.0.1")

    // Optional: advertising id (GAID) and App Set ID
    implementation("com.adfacten:ads-gaid:1.0.1")
    // Optional: video ads (Media3 ExoPlayer). Recommended if you use rewarded ads.
    implementation("com.adfacten:ads-video:1.0.1")
}

3.2 Initialize

Call once in Application.onCreate. It does no work on the main thread.

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Adfacten.initialize(
            this,
            "apk_XXXXXXXXXXXXXXXXXXXXXXXX",
            AdfactenConfig.Builder()
                .setTestMode(BuildConfig.DEBUG)   // false in release builds
                .setLogLevel(LogLevel.WARN)
                .setChildDirected(false)
                .build()
        )
    }
}

When the user signs in (for rewarded ads), pass your user id:

Adfacten.setUserId("user-123")   // arrives as user_id in the server-side reward callback

3.3 Consent

The SDK reads by itself the values your IAB-compliant consent tool (CMP) stores (IABTCF_gdprApplies, IABTCF_TCString, IABTCF_PurposeConsents, IABGPP_HDR_GppString, IABUSPrivacy_String). Nothing else to do; run your CMP before loading ads. Details: section 6.

3.4 Banner

In XML:

<com.adfacten.ads.AdfactenBannerView
    android:id="@+id/banner"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:adf_placementId="plc_XXXXXXXX"
    app:adf_adSize="banner" />

adf_adSize: banner (320x50), large_banner (320x100), medium_rectangle (300x250), leaderboard (728x90, tablets).

In code:

val banner = AdfactenBannerView(context).apply {
    placementId = "plc_XXXXXXXX"
    adSize = AdSize.BANNER
    listener = object : BannerAdListener {
        override fun onLoaded() {}
        override fun onFailed(error: AdError) { Log.w("ads", error.toString()) }
    }
}
container.addView(banner)
banner.load()

// Activity/Fragment lifecycle:
override fun onPause()   { banner.pause();   super.onPause() }
override fun onResume()  { super.onResume(); banner.resume() }
override fun onDestroy() { banner.destroy(); super.onDestroy() }

While visible, the banner refreshes itself at the placement's interval.

3.5 Interstitial

private var interstitial: InterstitialAd? = null

fun loadInterstitial() {
    InterstitialAd.load(context, "plc_XXXXXXXX", object : AdLoadCallback<InterstitialAd> {
        override fun onLoaded(ad: InterstitialAd) { interstitial = ad }
        override fun onFailed(error: AdError) { interstitial = null }
    })
}

fun showInterstitial(activity: Activity) {
    val ad = interstitial ?: return
    if (ad.isExpired) { loadInterstitial(); return }
    ad.listener = object : FullScreenAdListener {
        override fun onClosed() { loadInterstitial() }   // preload the next one
    }
    ad.show(activity)
    interstitial = null   // an ad can be shown only once
}

3.6 Rewarded

RewardedAd.load(context, "plc_XXXXXXXX", object : AdLoadCallback<RewardedAd> {
    override fun onLoaded(ad: RewardedAd) { rewarded = ad }
    override fun onFailed(error: AdError) { rewarded = null }
}, customData = "level=12")   // optional, ≤ 256 chars, echoed as 'custom' in the server callback

rewarded?.show(activity) { reward ->
    // UI only (e.g. "Your reward is on its way").
    // Credit the user from the callback YOUR SERVER receives (section 6).
}

3.7 Native

Put your own layout inside a NativeAdView and tell it which view shows which asset:

<com.adfacten.ads.NativeAdView
    android:id="@+id/native_ad"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:adf_titleView="@id/ad_title"
    app:adf_bodyView="@id/ad_body"
    app:adf_iconView="@id/ad_icon"
    app:adf_imageView="@id/ad_image"
    app:adf_callToActionView="@id/ad_cta"
    app:adf_adLabelView="@id/ad_label">
    <!-- your own TextView / ImageView / Button views -->
</com.adfacten.ads.NativeAdView>
NativeAd.load(context, "plc_XXXXXXXX", object : AdLoadCallback<NativeAd> {
    override fun onLoaded(ad: NativeAd) { nativeAdView.setNativeAd(ad) }
    override fun onFailed(error: AdError) { nativeAdView.visibility = View.GONE }
})

NativeAdView measures the impression, viewability and clicks itself. Always keep the "Ad" label visible. Call ad.destroy() when the view goes away.

3.8 App open

AppOpenAd.load(context, "plc_XXXXXXXX", object : AdLoadCallback<AppOpenAd> {
    override fun onLoaded(ad: AppOpenAd) { appOpen = ad }
    override fun onFailed(error: AdError) {}
})

// When the app comes to the foreground:
appOpen?.takeIf { !it.isExpired }?.show(activity)

Show the app open ad after your splash screen on a cold start, or when the app returns from the background; do not interrupt the user mid-task.

3.9 Google Play "Data safety" form

Data the SDK collects and sends to Adfacten (combine it with your own app's data when filling in the form):

Data When Purpose
Device or other IDs: SDK install id Always Advertising, fraud prevention
Device or other IDs: advertising id (GAID), App Set ID Only with ads-gaid and when allowed Advertising, analytics
App interactions (ad impressions, clicks) Always Advertising, analytics
User ID (userId) Only if you set it (rewarded) App functionality (rewards)
Approximate location (country) derived from the IP address On the server Ad targeting, fraud prevention

Data is encrypted in transit (HTTPS) and shared with a third party (Adfacten).

4. iOS

Requirements: iOS 15 or later. Module: AdfactenAds. No third-party dependencies; video uses AVPlayer.

4.1 Installation

The SDK is distributed as a prebuilt XCFramework. Pick one of three ways:

Source code: https://github.com/adfacten/adfacten-ios-sdk (Apache 2.0).

Swift Package Manager: Xcode → File → Add Package Dependencies… → enter https://github.com/adfacten/adfacten-ios-sdk and add the AdfactenAds product.

// Projects with a Package.swift:
.package(url: "https://github.com/adfacten/adfacten-ios-sdk.git", from: "1.0.0")

CocoaPods:

pod 'AdfactenAds', :git => 'https://github.com/adfacten/adfacten-ios-sdk.git', :tag => '1.0.0'

Manual (XCFramework): download https://cdn.adfacten.com/ios/AdfactenAds-1.0.0.xcframework.zip, unzip it and add it to your target as Embed & Sign. SHA-256: 6ab9883dc8d92597f2f0c7391c4b2434c15950548e7e465e16367539290b883f

To try it right away, run the sample app in the repository: it ships with the keys of Adfacten's public test app and shows (unbilled) test ads with no setup.

4.2 Initialize

Call once at launch, on the main thread:

import AdfactenAds

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let config = AdfactenConfig()
    #if DEBUG
    config.testMode = true            // false in release builds
    #endif
    config.childDirected = false
    Adfacten.initialize(appKey: "apk_XXXXXXXXXXXXXXXXXXXXXXXX", config: config)
    return true
}

// When the user signs in (for rewarded ads):
Adfacten.setUserId("user-123")

4.3 App Tracking Transparency (ATT) and the privacy manifest

4.4 Banner

let banner = AdfactenBannerView(placementId: "plc_XXXXXXXX", size: .banner)
banner.rootViewController = self
banner.delegate = self   // AdfactenBannerViewDelegate (optional)
view.addSubview(banner)  // it knows its own size (intrinsicContentSize)
banner.load()

Sizes: .banner (320x50), .largeBanner (320x100), .mediumRectangle (300x250), .leaderboard (728x90, tablets). In SwiftUI:

AdfactenBanner(placementId: "plc_XXXXXXXX", size: .banner)

4.5 Interstitial

var interstitial: InterstitialAd?

func loadInterstitial() {
    InterstitialAd.load(placementId: "plc_XXXXXXXX") { [weak self] ad, error in
        self?.interstitial = ad
    }
}

func showInterstitial() {
    guard let ad = interstitial, ad.isReady else { loadInterstitial(); return }
    ad.onDismissed = { [weak self] in self?.loadInterstitial() }
    ad.present(from: self)
    interstitial = nil
}

There is an async variant too: let ad = try await InterstitialAd.load(placementId: "plc_…").

4.6 Rewarded

RewardedAd.load(placementId: "plc_XXXXXXXX", customData: "level=12") { [weak self] ad, error in
    self?.rewarded = ad
}

rewarded?.present(from: self) { reward in
    // UI only. Credit the user from the callback your server receives (section 6).
}

4.7 Native

In your NativeAdView subclass (in code or Interface Builder) connect the outlets titleLabel, bodyLabel, brandLabel, callToActionButton, mediaView, iconView, adLabel.

NativeAd.load(placementId: "plc_XXXXXXXX") { [weak self] ad, error in
    self?.nativeAdView.nativeAd = ad   // binds the assets, measures impression and clicks
}

Set nativeAdView.nativeAd = nil when a cell is reused. If you connect no adLabel, the SDK adds its own "Ad" badge.

4.8 App open

AppOpenAd.load(placementId: "plc_XXXXXXXX") { [weak self] ad, error in
    self?.appOpen = ad
}

// When the app comes to the foreground:
if let ad = appOpen, ad.isReady { ad.present() }

5. Flutter, React Native and Unity

Official packages built on the Android and iOS SDKs; formats, test mode and reward callbacks work as described above.

5.1 Flutter

Source code and sample app: https://github.com/adfacten/adfacten_ads_flutter (Apache 2.0). pubspec.yaml:

dependencies:
  adfacten_ads:
    git:
      url: https://github.com/adfacten/adfacten_ads_flutter
      ref: 1.0.1
await Adfacten.initialize('apk_XXXXXXXXXXXXXXXXXXXXXXXX');

AdfactenBanner(placementId: 'plc_XXXXXXXX', size: AdSize.banner);

final ad = await RewardedAd.load('plc_XXXXXXXX');
await ad.show(onUserEarnedReward: (reward) => grant(reward));

Native ads are drawn with the SDK's templates (small row, medium card); that is how impressions and clicks are counted correctly. See the package README for details.

5.2 React Native and Expo

Source code and sample app: https://github.com/adfacten/adfacten-react-native-ads (Apache 2.0). React Native 0.80+ (New Architecture).

npm install github:adfacten/adfacten-react-native-ads#1.0.1
cd ios && pod install
import { Adfacten, AdfactenBanner, RewardedAd } from '@adfacten/react-native-ads';

Adfacten.initialize('apk_XXXXXXXXXXXXXXXXXXXXXXXX');
<AdfactenBanner placementId="plc_XXXXXXXX" size="banner" />;

Expo: add "plugins": ["@adfacten/react-native-ads"] to app.json and use npx expo prebuild or a dev client. Expo Go can't load native modules, so it is not supported.

Both sample apps ship with the keys of Adfacten's public test app and show (unbilled) test ads with no setup.

5.3 Unity

Source code and demo scene: https://github.com/adfacten/adfacten-unity-ads (Apache 2.0). Unity 2021.3 LTS or later. In Window › Package Manager › + › Add package from git URL, enter:

https://github.com/adfacten/adfacten-unity-ads.git#1.0.0

Android and iOS dependencies are installed by External Dependency Manager (EDM4U); enter your app keys in Project Settings › Adfacten.

Adfacten.Initialize("apk_XXXXXXXXXXXXXXXXXXXXXXXX");

var banner = new BannerView("plc_XXXXXXXX", AdSize.Banner, AdPosition.Bottom);
banner.Load();

RewardedAd.Load("plc_XXXXXXXX", (ad, error) => {
    if (ad != null) ad.Show(reward => { /* UI only; credit the user from your server's callback */ });
});

In the Editor you get placeholder ads, so you can build the game without a device.

6. Rewarded ads: server-side verification

Credit rewards only from the Adfacten callback your server receives. The in-app onUserEarnedReward callback is for UI only; a tampered app can fire it at will.

6.1 Setup

Panel → Apps → your app → Reward callback:

6.2 The callback

When a user validly completes a rewarded ad, Adfacten sends a GET request to your server:

GET https://api.yourgame.com/adfacten/reward?env=prod&user_id=user-123&custom=level%3D12&reward_type=coins&reward_amount=10&transaction_id=imp_…&placement=plc_XXXXXXXX&ts=1794160000&sig=5f1c…e9
Parameter Value
(your parameters) Whatever your callback URL contains, first
user_id The id set with Adfacten.setUserId / userId (max 128 chars; empty if none)
custom The customData passed when loading the ad (max 256 chars; empty if none)
reward_type The placement's reward type, e.g. coins
reward_amount The placement's reward amount (integer)
transaction_id Unique id of this impression. One per reward.
placement Placement id (plc_…)
ts When the reward was earned (Unix seconds). Unchanged on retries.
test Test ads only: 1
sig Signature, always last

Parameters arrive in this order and values are percent-encoded (e.g. a space is %20).

6.3 Verifying the signature

sig = hex( HMAC-SHA256( secret, raw_query ) )

raw_query is the query string exactly as received: everything after ?, minus the trailing &sig=…. Your own parameters (if any) and test=1 (if present) are covered by the signature.

Node.js (Express)

import crypto from "node:crypto";

const SECRETS = [process.env.ADFACTEN_REWARD_SECRET, process.env.ADFACTEN_REWARD_SECRET_OLD].filter(Boolean);

function verifyAdfacten(rawQuery) {
  const i = rawQuery.lastIndexOf("&sig=");
  if (i < 0) return false;
  const signed = rawQuery.slice(0, i);
  const sig = rawQuery.slice(i + 5);
  if (!/^[0-9a-f]{64}$/.test(sig)) return false;
  const given = Buffer.from(sig, "hex");
  return SECRETS.some((s) =>
    crypto.timingSafeEqual(crypto.createHmac("sha256", s).update(signed, "utf8").digest(), given));
}

app.get("/adfacten/reward", async (req, res) => {
  const url = req.originalUrl;
  const raw = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
  if (!verifyAdfacten(raw)) return res.sendStatus(403);
  const q = new URLSearchParams(raw);
  await grantOnce(q.get("transaction_id"), q.get("user_id"), q.get("reward_type"),
                  Number(q.get("reward_amount")), q.get("test") === "1");
  res.sendStatus(200);
});

Python (FastAPI / Starlette)

import hashlib, hmac, os, re
from urllib.parse import parse_qs
from fastapi import FastAPI, Request, Response

SECRETS = [s for s in (os.environ.get("ADFACTEN_REWARD_SECRET"), os.environ.get("ADFACTEN_REWARD_SECRET_OLD")) if s]

def verify_adfacten(raw_query: str) -> bool:
    i = raw_query.rfind("&sig=")
    if i < 0:
        return False
    signed, sig = raw_query[:i], raw_query[i + 5:]
    if not re.fullmatch(r"[0-9a-f]{64}", sig):
        return False
    return any(
        hmac.compare_digest(hmac.new(s.encode(), signed.encode(), hashlib.sha256).hexdigest(), sig)
        for s in SECRETS
    )

app = FastAPI()

@app.get("/adfacten/reward")
async def reward(request: Request) -> Response:
    raw = request.scope["query_string"].decode("latin-1")   # Flask: request.query_string.decode()
    if not verify_adfacten(raw):
        return Response(status_code=403)
    q = {k: v[0] for k, v in parse_qs(raw, keep_blank_values=True).items()}
    await grant_once(q["transaction_id"], q["user_id"], q["reward_type"], int(q["reward_amount"]), q.get("test") == "1")
    return Response(status_code=200)

PHP

<?php
function verify_adfacten(string $raw, array $secrets): bool {
    $i = strrpos($raw, '&sig=');
    if ($i === false) return false;
    $signed = substr($raw, 0, $i);
    $sig = substr($raw, $i + 5);
    if (!preg_match('/^[0-9a-f]{64}$/', $sig)) return false;
    foreach ($secrets as $s) {
        if (hash_equals(hash_hmac('sha256', $signed, $s), $sig)) return true;
    }
    return false;
}

$uri = $_SERVER['REQUEST_URI'];                       // raw URI (QUERY_STRING may have been rewritten)
$raw = ($p = strpos($uri, '?')) === false ? '' : substr($uri, $p + 1);
$secrets = array_filter([getenv('ADFACTEN_REWARD_SECRET'), getenv('ADFACTEN_REWARD_SECRET_OLD')]);

if (!verify_adfacten($raw, $secrets)) { http_response_code(403); exit; }
parse_str($raw, $q);
grant_once($q['transaction_id'], $q['user_id'], $q['reward_type'], (int)$q['reward_amount'], ($q['test'] ?? '') === '1');
http_response_code(200);

Go

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"net/url"
	"os"
	"strings"
)

var secrets = nonEmpty(os.Getenv("ADFACTEN_REWARD_SECRET"), os.Getenv("ADFACTEN_REWARD_SECRET_OLD"))

func verifyAdfacten(raw string) bool {
	i := strings.LastIndex(raw, "&sig=")
	if i < 0 {
		return false
	}
	signed, given := raw[:i], raw[i+5:]
	want, err := hex.DecodeString(given)
	if err != nil || len(want) != sha256.Size {
		return false
	}
	for _, s := range secrets {
		m := hmac.New(sha256.New, []byte(s))
		m.Write([]byte(signed))
		if hmac.Equal(m.Sum(nil), want) {
			return true
		}
	}
	return false
}

func rewardHandler(w http.ResponseWriter, r *http.Request) {
	raw := r.URL.RawQuery
	if !verifyAdfacten(raw) {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	q, _ := url.ParseQuery(raw)
	if err := grantOnce(r.Context(), q.Get("transaction_id"), q.Get("user_id"), q.Get("reward_type"),
		q.Get("reward_amount"), q.Get("test") == "1"); err != nil {
		w.WriteHeader(http.StatusInternalServerError) // transient error: Adfacten retries
		return
	}
	w.WriteHeader(http.StatusOK)
}

6.4 Never grant the same reward twice

6.5 Responses, retries, log

6.6 Test ads

Reward callbacks for test ads carry test=1. They are for testing your integration: do not grant real value for them (or grant it only in a test environment).

6.7 Rotating the secret

Panel → Reward callback → New secret. The new secret takes effect immediately and is shown once; pending retries are signed with the new secret too. For a seamless switch:

  1. Get the new secret and add it to your server alongside the old one (…_OLD in the examples above).
  2. Remove the old secret a few hours later.

Callbacks whose signature fails during the switch are not lost; they are retried.

7. Privacy and consent

Situation What the SDK / server does
GDPR / TCF: the CMP stored IABTCF_gdprApplies=1 The advertising id (GAID/IDFA) is not used; frequency and event binding use the SDK install id; device-id macros in click URLs stay empty. Without consent for Purpose 1 (store/access information on a device) the SDK does not send the advertising id or IDFV at all.
GPP: IABGPP_HDR_GppString is set The advertising id is not used; no device-id macros.
US privacy: the 3rd character of IABUSPrivacy_String is Y (opt-out of sale) Same: the advertising id is not used.
Limited ad tracking (LMT): advertising id deleted / limited on Android, no ATT permission on iOS The advertising id is not used; ads keep serving.
COPPA: childDirected = true or the app is child-directed in the panel No ads; the server answers 403 coppa.

8. Direct REST (custom integrations / mediation)

If you cannot use the SDK (your own mediation layer, a game-engine plugin…) you can call the same endpoints directly. This is a public, backward-compatible interface: fields are only added; ignore fields you do not know.

8.1 Ad request

POST https://trk.adfacten.com/m/v1/ad
Content-Type: application/json
User-Agent: AdfactenSDK/1.0.0 (android 15; SM-A546B) com.company.app/1.2.3
X-Adfacten-SDK: android/1.0.0
{
  "v": 1,
  "app_key": "apk_XXXXXXXXXXXXXXXXXXXXXXXX",
  "placement": "plc_XXXXXXXX",
  "format": "banner",
  "sizes": ["320x50"],
  "device": {
    "os": "android", "osv": "15", "make": "Samsung", "model": "SM-A546B",
    "sdk_id": "3f6c0a52-8f7e-4d1b-9a0c-1b2c3d4e5f60",
    "ifa": "", "ifa_type": "sdk", "lmt": false,
    "lang": "en", "w": 1080, "h": 2340, "pxratio": 2.75,
    "connection": "wifi", "carrier": "", "tz_offset_min": 180
  },
  "app": { "bundle": "com.company.app", "version": "1.2.3", "sdk_version": "1.0.0" },
  "regs": { "coppa": false, "gdpr": 0, "tcf": "", "gpp": "", "us_privacy": "" },
  "user": { "id": "user-123", "custom": "level=12" },
  "test": false
}

8.2 Response

{
  "ad": {
    "id": "opaque, signed impression token",
    "format": "banner",
    "kind": "image",
    "test": false,
    "w": 320, "h": 50,
    "image": { "url": "https://cdn.adfacten.com/….webp", "url_2x": "…", "fallback": "….jpg" },
    "click_url": "https://trk.adfacten.com/a/click?imp=…",
    "trackers": {
      "render":   "https://trk.adfacten.com/i?imp=…&e=render",
      "view":     "https://trk.adfacten.com/i?imp=…&e=view",
      "complete": "https://trk.adfacten.com/i?imp=…&e=complete",
      "close":    "https://trk.adfacten.com/i?imp=…&e=close"
    },
    "view_rule": { "min_ratio": 0.5, "min_ms": 1000 },
    "close_after_s": 0,
    "reward": { "type": "coins", "amount": 10 },
    "label": "Reklam",
    "expires_at": 1794160000
  }
}

Content by kind:

kind Field How to render
image image An image; on tap open click_url
html html A sandboxed WebView with no JavaScript bridge. The WebView must not navigate itself: intercept every navigation and window.open and open it externally (browser).
video vast VAST 4.1 (interstitial / rewarded).
native native Assets title, body, brand, cta, image, icon

8.3 Event beacons

Append &sid=<device.sdk_id>&ts=<milliseconds> to each tracker URL and send a GET (or a POST with an empty body). sid must be the same sdk_id that requested the ad; events with another sid are not billed.

Event When
render When the ad is actually drawn on screen. This is the billable impression.
view When view_rule is met (≥ 50% visible for ≥ min_ms; 2 s for video)
complete When a video is watched to the end. For non-video rewarded ads: once close_after_s has elapsed while the ad was on screen. The reward depends on it; early complete events are ignored.
close When the user closes the ad (accepted, not billed)

8.4 Error codes

Failed requests return {"code": "...", "message": "..."}. Do not retry these; retry with backoff only on 5xx and network errors.

HTTP code Meaning
400 invalid_request Unreadable body or a required field is missing
400 placement_mismatch Unknown placement, a placement of another app, or format differs from the placement's format
403 unknown_app No such app key
403 app_not_approved The app was rejected
403 platform_mismatch The key belongs to the other platform (e.g. an iOS key on Android)
403 bundle_mismatch app.bundle differs from the registered bundle id
403 coppa The app is child-directed or regs.coppa is set
403 test_only Reserved (not sent in v1)

9. Troubleshooting

In the SDK, the error's serverCode carries the server code above. For detailed logs set logLevel temporarily to LogLevel.DEBUG on Android or .debug on iOS (Android Logcat tag: Adfacten).

Symptom Cause Fix
unknown_app The app key is mistyped or belongs to another environment Copy the apk_… key from the panel
bundle_mismatch The build's package name (e.g. a debug build with a .debug suffix) differs from the registered one Build with the registered bundle id; avoid applicationIdSuffix in debug builds
platform_mismatch Android key used on iOS (or vice versa) Add one app per platform and use each one's key
placement_mismatch Wrong placement id, another app's placement, an archived one, or a format mismatch (e.g. a banner placement loaded with RewardedAd) Use the right plc_… id and the matching ad class
app_not_approved The app was rejected Read the reason in the panel, fix it and contact support
coppa childDirected is on or the app is marked child-directed Adfacten cannot be used in child-directed apps
Always test ads One of the conditions in 2.3 Check Serving status in the panel: pending review → wait for approval; app-ads.txt → add the line, Check again; test mode → switch it off; SDK testMode on → switch it off; emulator → use a real device
No ads (204; Android NO_FILL, iOS .noFill) No matching ad right now, paused placement, daily cap reached, or the traffic looks like a bot / data centre Normal; the SDK waits at least 30 s before asking again. Check that the placement is active and test on a real device without a VPN
VIDEO_NOT_SUPPORTED (Android) A video ad arrived but ads-video is not included Add the com.adfacten:ads-video dependency
Android AD_EXPIRED, iOS .expired The ad was not shown within about 55 minutes of loading Load shortly before showing; load the next one on close
No reward callbacks No callback URL set, the URL is not https, your server does not return 2xx, or signature verification fails Read the status code and error in the reward callback log in the panel