The Axeptio SDK for Android — collect, manage and surface user consents natively in your app.
- Two cookie flows - Brands, or Publisher following the IAB TCF standard, resolved from your remote Axeptio configuration.
- TCF compliant - exposes the TC string and per-vendor consents for third-party SDKs that expect them.
- System permissions - request Android runtime permissions (camera, location, notifications, and more) from the same configurable flow.
- Persistent syncing - consent decisions are cached locally and retried against the Axeptio backend with exponential backoff if a sync fails. Unsynced consents retry automatically on the next launch.
- Consent state at hand - query consent status, the TC string, and per-vendor consents at any
time, or observe
consentStatusFlowfor changes. - 26 languages built in.
- Android 13 (API 33) or later (
minSdk 33) compileSdk 36or later, with an Android Gradle Plugin version that supports it- JDK 21 (the SDK is compiled to Java 21 bytecode)
- Kotlin 2.2 or later (the SDK is built with Kotlin 2.3; older compilers cannot read its metadata)
The SDK declares the android.permission.INTERNET permission itself; Gradle merges it into your
app's manifest automatically, so you don't need to add it.
The public API is designed for Kotlin. Calling it from Java is not supported (it relies on
suspend functions, Flow, kotlin.Result and a lambda-with-receiver builder).
You need an Axeptio project set up for mobile in the Axeptio back-office. The SDK is initialized with:
| Parameter | Required | What it is |
|---|---|---|
projectId |
Yes | Your Axeptio project identifier, from the back-office. |
appVersion |
Yes | Sent with every configuration request as the version parameter. Use your app's version name. |
token |
No | Your project's API token, from the back-office. When set, it is sent as a Bearer authorization header on every request. |
targetService |
No | AxeptioService.Brands (default) or AxeptioService.Publisher (IAB TCF). Must match the configurations defined in your project. |
configId |
No | Pins one configuration of the project — see Configuration ID. |
If you are unsure which values to use, contact Axeptio support.
The SDK is published as Android libraries (AAR) to a public Maven repository on GitHub. Add the repository and the dependency, and Gradle resolves the SDK and everything it needs transitively.
Add the Axeptio Maven repository to your settings.gradle.kts (or top-level build.gradle.kts):
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://raw.githubusercontent.com/axeptio/native-android-sdk/master/maven")
}
}
}Or using Gradle Groovy DSL in settings.gradle:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://raw.githubusercontent.com/axeptio/native-android-sdk/master/maven' }
}
}The repository serves every released version side by side — you select the version with the dependency coordinate below, not in the URL.
Then add the dependency to your app module's build.gradle.kts:
dependencies {
implementation("io.axeptio:sdk:1.1.0")
}Or using Gradle Groovy DSL in build.gradle:
dependencies {
implementation 'io.axeptio:sdk:1.1.0'
}Check the releases page for the latest
version and the release notes of each. Pre-releases (for example
X.Y.Z-beta.N) are published to the same repository and are opt-in: pin them explicitly to try
upcoming features. A new release can take a few minutes to become resolvable, because
raw.githubusercontent.com caches files.
To keep Gradle from querying the Axeptio repository for every other dependency, you can restrict it
to the io.axeptio group:
maven {
url = uri("https://raw.githubusercontent.com/axeptio/native-android-sdk/master/maven")
content { includeGroup("io.axeptio") }
}That single coordinate transitively pulls in the SDK's internal modules and its runtime dependencies. You do not declare anything else, but Gradle resolves each of them to the highest version requested in your build, so check for conflicts if your app uses the same libraries:
| Library | Version used by the SDK |
|---|---|
| Ktor client (OkHttp) | 3.4 |
| Koin | 4.2 |
| Coil | 3.4 |
| Kotlin coroutines | 1.10 |
| kotlinx.serialization | 1.11 |
| AndroidX / Compose | Compose BOM 2026.03 |
The SDK runs its own isolated Koin instance, so it never touches your app's global Koin container.
The SDK ships its own R8/ProGuard consumer rules inside the AAR. If your release build enables minification, no extra configuration is needed — Gradle applies the SDK's rules automatically.
Initialize the SDK once before using any other method — for example in your Application.onCreate():
import android.app.Application
import io.axeptio.sdk.AxeptioSDK
import io.axeptio.sdk.configuration.AxeptioService
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AxeptioSDK.initialize(this) {
projectId = "your-project-id"
appVersion = "1.0.0" // your app's version name
token = "your-api-token"
targetService = AxeptioService.Brands
// configId = "your-config-id" // optional — see below
}
}
}Register it in your AndroidManifest.xml: <application android:name=".MyApplication" …>.
Use AxeptioService.Publisher instead for the IAB TCF flow:
AxeptioSDK.initialize(this) {
projectId = "your-project-id"
appVersion = "1.0.0"
token = "your-api-token"
targetService = AxeptioService.Publisher
}If targetService is omitted, the SDK defaults to AxeptioService.Brands.
An Axeptio project can hold several configurations. By default the SDK picks one for you:
- Brands — the configuration matching the user's location (and device language), resolved by Axeptio's geolocation service. If that lookup fails, the project's default configuration.
- Publisher (TCF) — the TCF configuration whose language matches the device language, otherwise the first TCF configuration of the project.
Pass configId to always use a specific configuration instead:
AxeptioSDK.initialize(this) {
projectId = "your-project-id"
appVersion = "1.0.0"
token = "your-api-token"
configId = "your-config-id"
}If configId does not match a configuration of the selected targetService, the SDK logs a
warning and resolves one automatically as described above.
Note: consent is stored per configuration. When the resolved configuration changes (for example you change
configId, or the user moves to a region served by another configuration), the stored consent is discarded and the user is asked again.
By default the SDK talks to Axeptio's production backend. Pass environment to point it at
Axeptio's staging backend instead — only do this if Axeptio has set up your project there:
import io.axeptio.sdk.configuration.AxeptioEnvironment
AxeptioSDK.initialize(this) {
projectId = "your-project-id"
appVersion = "1.0.0"
token = "your-api-token"
environment = AxeptioEnvironment.Staging
}When environment is omitted the SDK defaults to AxeptioEnvironment.Production.
Pass the permissions you want the SDK to manage during the consent flow:
import android.app.Application
import io.axeptio.sdk.AxeptioSDK
import io.axeptio.sdk.configuration.AxeptioPermission
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AxeptioSDK.initialize(this) {
projectId = "your-project-id"
appVersion = "1.0.0"
token = "your-api-token"
withPermissions(
listOf(
AxeptioPermission.Camera(),
AxeptioPermission.Microphone(
title = "Microphone Access",
description = "Used for voice features"
),
AxeptioPermission.Notifications(),
AxeptioPermission.LocationFine(
title = "Precise Location",
description = "Used to show nearby content"
)
)
)
}
}
}Each permission type may appear only once: passing duplicates makes initialize() throw
IllegalArgumentException.
title/description are optional and only control the copy shown on the SDK's own rationale card,
displayed before the native Android permission dialog — if omitted, the SDK falls back to its own
localized default text. Unlike iOS, Android has no manifest key that's required alongside
initialize() (there's no Android equivalent of NSCameraUsageDescription); the only thing the OS
itself requires is the <uses-permission> declaration below.
The SDK requests permissions at runtime, but you must still declare the corresponding Android
permissions in your app's AndroidManifest.xml. Each AxeptioPermission type maps to the following
manifest permission(s):
AxeptioPermission |
Manifest permission(s) to declare |
|---|---|
Camera |
android.permission.CAMERA |
Microphone |
android.permission.RECORD_AUDIO |
Notifications |
android.permission.POST_NOTIFICATIONS |
LocationFine |
android.permission.ACCESS_FINE_LOCATION, android.permission.ACCESS_COARSE_LOCATION |
Contacts |
android.permission.READ_CONTACTS, android.permission.WRITE_CONTACTS |
Calendar |
android.permission.READ_CALENDAR, android.permission.WRITE_CALENDAR |
Bluetooth |
android.permission.BLUETOOTH_SCAN, android.permission.BLUETOOTH_CONNECT |
PhotoLibrary |
android.permission.READ_MEDIA_IMAGES, android.permission.READ_MEDIA_VIDEO, android.permission.READ_MEDIA_VISUAL_USER_SELECTED (Android 14+) |
BodySensors |
android.permission.BODY_SENSORS |
PhoneAccount |
android.permission.READ_PHONE_STATE |
Fitness |
android.permission.ACTIVITY_RECOGNITION |
For example, to use AxeptioPermission.Camera() and AxeptioPermission.LocationFine(), declare in
your manifest:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />Control SDK log output (logcat tag AxeptioSDK) via loggerLevel at initialization:
import io.axeptio.sdk.model.AxeptioLogLevel
AxeptioSDK.initialize(this) {
// ...
loggerLevel = AxeptioLogLevel.DEBUG
}| Level | Output |
|---|---|
NONE (default) |
Nothing |
DEBUG |
Initialization, the selected configuration, consent saved, IAB TCF values written, warnings and errors |
Collect the consent status in your Activity and show the consent flow when it is needed. Use
repeatOnLifecycle so collection stops while the activity is in the background:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import io.axeptio.sdk.AxeptioSDK
import io.axeptio.sdk.model.ConsentStatus
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
AxeptioSDK.consentStatusFlow.collect { status ->
if (status is ConsentStatus.Ready && status.shouldDisplayConsents) {
AxeptioSDK.showConsentFlow(this@MainActivity)
}
}
}
}
}
}In a Fragment, launch from viewLifecycleOwner.lifecycleScope and collect within
viewLifecycleOwner.repeatOnLifecycle(...), passing requireActivity() to showConsentFlow.
Once the user completes the flow and the consent is stored, the status changes to Ready(false) —
observe that transition if you need to know when to initialize consent-dependent SDKs.
The SDK renders its own screens from a ComponentActivity — your app does not need to use Jetpack
Compose. Only call these after initialize(): calling one before initialize() (or after
shutdown()) throws SdkNotInitializedException. Initializing in Application.onCreate(), before
any activity can call these, is the simplest way to avoid it.
import io.axeptio.sdk.AxeptioSDK
import io.axeptio.sdk.model.SdkNotInitializedException
// Show the first-run consent flow
try {
AxeptioSDK.showConsentFlow(activity)
} catch (e: SdkNotInitializedException) {
// AxeptioSDK.initialize() hasn't been called yet
}
// Open the consent manager (for updates / re-consent)
AxeptioSDK.showConsentManager(activity)
// Open the permissions screen
AxeptioSDK.showPermissionsScreen(activity)| Status | Meaning | Recommended action |
|---|---|---|
Ready(true) |
Consent is required (new, changed or expired) | Call showConsentFlow() |
Ready(false) |
Consent is current and up to date | No action needed |
ConfigFetchFailed |
Could not fetch the configuration and none is cached | Retry on next app initialization |
NotInitialized |
initialize() has not been called, or shutdown() was |
Call AxeptioSDK.initialize() (again, after shutdown()) |
Note: right after
initialize(), nothing is emitted until the configuration has loaded or failed — an earlier collector keeps seeingNotInitializeduntil then. Don't callinitialize()again in response: the SDK is already initialized at that point, so a second call throwsSdkAlreadyInitializedException— the flow catches up on its own once loading finishes.
The SDK provides methods to query the current consent state and associated data. They can be called from any thread and deliver their result through a callback.
Threading: callbacks are invoked on a background thread, except when the SDK isn't initialized: the
SdkNotInitializedExceptionfailure is then delivered synchronously on the calling thread. Otherwise, switch to the main thread before touching your UI, for example withrunOnUiThread { … }orlifecycleScope.launch(Dispatchers.Main) { … }.
// Days until the stored consent expires, computed against ttlDays (negative once expired).
// Returns ttlDays when no consent is stored yet. This is informational only: the SDK itself
// asks for consent again after 190 days, whatever value you pass here.
AxeptioSDK.getRemainingDaysForConsent(ttlDays = 190) { result ->
result.onSuccess { days -> /* Int */ }
}
// Get the unique Axeptio user token
AxeptioSDK.getAxeptioToken { result ->
result.onSuccess { token -> /* String? */ }
}
// Every vendor with a stored choice (Brands flow), keyed by display name — true or false
AxeptioSDK.getBrandsVendorConsents { result ->
result.onSuccess { consents -> /* Map<String, Boolean> */ }
}
// Get TCF TC String
AxeptioSDK.getTcfTcString { result ->
result.onSuccess { tcString -> /* String? */ }
}
// Every disclosed vendor's consent (Publisher/TCF flow), keyed by IAB vendor id
AxeptioSDK.getTcfVendorConsents { result ->
result.onSuccess { consents -> /* Map<String, Boolean> */ }
}clearConsentData() is a suspend function, so call it from a coroutine:
lifecycleScope.launch {
val cleared: Boolean = AxeptioSDK.clearConsentData()
}It removes the stored consent — including, on the Publisher flow, the IABTCF_* values (below) —
and returns true on success, regardless of whether the configuration has finished loading yet.
consentStatusFlow then emits Ready(true) immediately, so an active collector shows the consent
flow again right away. It throws SdkNotInitializedException if the SDK is not initialized.
Call shutdown() to tear down the SDK — for example between test runs, or if you need to fully
reset state and re-initialize with different credentials. It detaches all reactive flows and
releases the SDK's internal dependency graph; a subsequent initialize() call starts clean.
Existing consentStatusFlow collectors stay subscribed: they receive NotInitialized
immediately, then the new status once you call initialize() again. Any open SDK screen (the
consent flow, consent manager or permissions screen) closes. Calling shutdown() when the SDK
isn't initialized is a no-op.
AxeptioSDK.shutdown()In the Publisher flow the SDK writes the standard IAB TCF v2 keys to the app's default
SharedPreferences (PreferenceManager.getDefaultSharedPreferences(context)), where ad and
analytics SDKs that support TCF read them automatically:
IABTCF_TCString, IABTCF_CmpSdkID, IABTCF_CmpSdkVersion, IABTCF_PolicyVersion,
IABTCF_PublisherCC, IABTCF_gdprApplies, IABTCF_PurposeOneTreatment,
IABTCF_UseNonStandardTexts, IABTCF_VendorConsents, IABTCF_VendorLegitimateInterests,
IABTCF_DisclosedVendors, IABTCF_PurposeConsents, IABTCF_PurposeLegitimateInterests,
IABTCF_SpecialFeaturesOptIns.
A few things changed since 1.0.1:
AxeptioPermissionimport. Already importing it fromio.axeptio.sdk.configuration? Nothing to do. Otherwise replaceimport io.axeptio.foundation.core.config.AxeptioPermissionwithimport io.axeptio.sdk.configuration.AxeptioPermission. The old import still compiles, butwithPermissions()then shows a deprecation warning, and that overload may be removed in a future major release. Three cases stop compiling until you switch:permissions = listOf(...)assigned directly: switch to the new import.- An untyped empty list,
withPermissions(emptyList())orwithPermissions(listOf()): add the type, e.g.withPermissions(emptyList<AxeptioPermission>()), or remove the call (no permissions is the default). - Reading
permissionTypeorandroidPermissionon a permission: usemanifestPermissionsinstead, which lists the coveredandroid.permission.*strings (allManifestPermissionsis still available too).
- INTERNET permission. The SDK now declares
android.permission.INTERNETitself. You can remove it from your own manifest if you only added it for the SDK — keeping it is harmless. - R8 / ProGuard rules. Consumer rules now ship inside the AAR. Remove any
-keep io.axeptiorules you had copied into your own ProGuard files. show*()beforeinitialize(). CallingshowConsentFlow(),showConsentManager()orshowPermissionsScreen()beforeinitialize()(or aftershutdown()) now throwsSdkNotInitializedException— see Showing the consent flow.
Calling initialize() more than once throws SdkAlreadyInitializedException — call shutdown()
first to re-initialize. Calling a query method or a show* method before initialize() (or after
shutdown()) throws SdkNotInitializedException for suspend functions and show* methods, or
delivers Result.failure(SdkNotInitializedException(...)) for callback-based functions - it's never
silently swallowed.
import io.axeptio.sdk.model.SdkNotInitializedException
AxeptioSDK.getAxeptioToken { result ->
result.onFailure { error ->
if (error is SdkNotInitializedException) {
// AxeptioSDK.initialize() hasn't been called yet
}
}
}consentStatusFlow never throws. Before initialize() and after shutdown() it emits
ConsentStatus.NotInitialized, then switches to live decisions once you call initialize() again
— no re-subscription needed. An unexpected internal error is different: it emits NotInitialized
once and then the flow completes, so collect it again (for example, the next
repeatOnLifecycle start) rather than treating that emission as the SDK being uninitialized.
The SDK is localized in 26 languages: English plus Bulgarian, Croatian, Czech, Danish, Dutch, Estonian, Finnish, French, German, Greek, Hungarian, Irish, Italian, Latvian, Lithuanian, Maltese, Norwegian Bokmål, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Spanish, and Swedish.
All 26 are bundled into the AAR and merged straight into your app by Gradle, so the SDK's screens resolve independently against the device's locale - they'll show, say, French on a French-language device even if your own app has no French strings at all.
Important: If your app strips locales at build time to reduce APK size (via
resourceConfigurationsorandroidResources.localeFiltersin Android Gradle Plugin), any locale excluded there is removed from the final APK entirely - including the SDK's - and falls back to the defaultvalues/(English) for those languages too.
The SDK talks to https://headless-api.axeptio.tech over HTTPS for configuration and consent
data. Vendor and brand images shown on the consent screens are downloaded separately, from
whatever hosts your Axeptio configuration references. Use this list when you fill in Google Play's
Data safety form:
| Purpose | What is sent |
|---|---|
| Configuration | Project ID, configuration ID, appVersion, device language |
| Location-based config | Project ID and device language (Brands flow; the server derives the region from the request's IP) |
| Consent records | The user's vendor choices, the Axeptio user token, appVersion, platform and configuration ID |
| TCF | Vendor list and TC string encoding requests (Publisher flow) |
| Usage analytics | Consent-flow events with timestamp, user agent, Axeptio user token, project and configuration IDs |
The Axeptio user token is issued by Axeptio's backend on first use and identifies the consent record; the SDK does not read the advertising ID.
This SDK replaces Axeptio's WebView-based
axeptio-android-sdk with native screens. The two
are separate products with different artifacts and APIs; do not include both.
WebView SDK (axeptio-android-sdk) |
Native SDK (this repository) |
|---|---|
AxeptioSDK.instance().initialize(activity, …) |
AxeptioSDK.initialize(context) { … }, once, in Application |
clientId |
projectId |
cookiesVersion |
configId (optional — resolved automatically when omitted) |
token (transfers an existing consent) |
Not supported. token is now your project's API token |
AxeptioService.PUBLISHERS_TCF / BRANDS |
AxeptioService.Publisher / AxeptioService.Brands |
showConsentScreen(activity) |
showConsentFlow(activity) / showConsentManager(activity) |
setEventListener (onPopupClosedEvent, onConsentSaved, …) |
Collect consentStatusFlow |
clearConsents() |
clearConsentData() |
getRemainingDaysForConsent() |
getRemainingDaysForConsent(ttlDays) { … } |
getVendorConsents() / isVendorConsented(id) |
getTcfVendorConsents { … } (keyed by IAB vendor id) / getBrandsVendorConsents { … } (keyed by display name) |
token / appendAxeptioToken(uri) for WebViews |
getAxeptioToken { … } (no URL helper) |
onGoogleConsentModeUpdate |
Not supported yet |
- Google Consent Mode v2 updates
- IAB GPP strings
- A consent-change listener with the user's choices (observe
consentStatusFlowinstead) - Sharing consent with WebViews through a URL helper
- Customizing the SDK's theme from code (the look comes from your Axeptio configuration)
- Java callers
A sample app is included alongside the published SDK to show it in action. It is a standalone Gradle
project that consumes the SDK from the bundled Maven repository (maven { url = uri("../maven") })
exactly as your own app would.
-
Clone the SDK repository:
git clone https://github.com/axeptio/native-android-sdk.git
-
Open the
sampleApp/folder in Android Studio (or run it from the command line). -
Choose a device or emulator and Run the
appconfiguration. It starts on a public Axeptio demo project; to use your own, change the defaults insampleApp/app/src/main/kotlin/io/axeptio/sample/config/AxeptioConfigManager.ktor use the in-app SDK Configuration screen:cd sampleApp && ./gradlew :app:installDebug
The sample app shows initialization, lifecycle-aware consent status handling, the consent flow, the
consent manager, the permissions screen, the consent queries and re-initialization with new
credentials. See sampleApp/README.md for details.
For integration questions, bug reports or feature requests, contact Axeptio support at support@axeptio.eu or visit the help centre. Release notes are on the releases page. To report a security vulnerability, see SECURITY.md.
The Axeptio Native Android SDK is distributed under Axeptio's licensing terms — see LICENSE.