Developer SDKs

SDK documentation

Learn how to integrate Digital Anumati with web, Android, and iOS experiences using platform-appropriate patterns.

Overview

Every SDK provides the same consent-management flow: initialize once with your site key and application ID, show the consent screen when the user hasn't decided yet, read the stored status, and let the user change or revoke consent at any time.

Web SDK

Add the Web SDK inside the <head> of your site so the widget is ready before any trigger fires. Pick your stack below and replace YOUR_SITE_KEY and YOUR_APPLICATION_ID with the values from your application in the dashboard.

Mount the consent SDK inside <head> using next/script with strategy="beforeInteractive" so the widget is ready before the first paint.

app/layout.tsx
// Next.js — add to app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Digital Anumati Consent SDK */}
<Script
src="https://cdn.digitalanumati.com/widget/uat/v1/anumati-dpdp-consent-v1.js"
data-site-key="YOUR_SITE_KEY"
application-id="YOUR_APPLICATION_ID"
strategy="beforeInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}

Next, mark the elements that should open a consent notice and verify each submission on your server — see Usage in the Developer Guide.

Android SDK (demo)

DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK

The package names, APIs, and endpoints in this section are placeholders that show how the integration is intended to work. They are not published, so the code won't resolve until the real SDK is released — the actual coordinates and endpoints will come from Digital Anumati.

Requirements: Android Studio, Kotlin, Gradle Kotlin DSL, Jetpack Compose, minSdk 24 or higher, and targetSdk 35. The examples below make up a complete sample app, AnumatiDemoApp, with a home screen that shows the SDK initialization state and consent status, plus Manage Consent and Revoke Consent buttons.

Installation

Register the repositories and the app module.

settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral() // the demo SDK would be published here
}
}
rootProject.name = "AnumatiDemoApp"
include(":app")
build.gradle.kts
plugins {
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}

Add the SDK dependency and expose the site key and application ID through BuildConfig.

app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.example.anumatidemo"
compileSdk = 35
defaultConfig {
applicationId = "com.example.anumatidemo"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
// Demo values. In a real app, inject these per build type or flavor from CI —
// never hard-code production values in source.
buildConfigField("String", "ANUMATI_SITE_KEY", "\"demo-site-key-123\"")
buildConfigField("String", "ANUMATI_APPLICATION_ID", "\"demo-mobile-app-123\"")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
buildFeatures {
compose = true
buildConfig = true
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
// DEMO / FICTIONAL SDK coordinates
implementation("com.digitalanumati:consent-sdk:1.0.0")
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
}

The SDK needs network access to sync consent with the service.

app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The SDK syncs consent with the Digital Anumati service over HTTPS. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".AnumatiDemoApplication"
android:label="AnumatiDemoApp"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Initialization

Initialize once, in your Application class. initialize() throws a ConsentError with code INVALID_CONFIGURATION, INVALID_SITE_KEY, or INVALID_APPLICATION_ID for a missing or malformed value; catch it so the app still starts.

AnumatiDemoApplication.kt
package com.example.anumatidemo
import android.app.Application
import android.util.Log
import com.digitalanumati.consent.ConsentError
import com.digitalanumati.consent.DigitalAnumati
import com.digitalanumati.consent.Environment
class AnumatiDemoApplication : Application() {
override fun onCreate() {
super.onCreate()
try {
DigitalAnumati.initialize(
context = applicationContext,
siteKey = BuildConfig.ANUMATI_SITE_KEY,
applicationId = BuildConfig.ANUMATI_APPLICATION_ID,
environment = if (BuildConfig.DEBUG) Environment.UAT else Environment.PRODUCTION,
)
} catch (error: ConsentError) {
// Keep the app usable. Consent actions stay disabled until initialization succeeds.
Log.w(TAG, "Consent SDK failed to initialize: " + error.message)
}
}
private companion object {
const val TAG = "AnumatiDemo"
}
}

The activity reads the stored status on launch, opens the consent screen when the user hasn't decided yet, and receives the ConsentResult through ConsentListener. Revoking runs in a coroutine because it syncs with the service.

MainActivity.kt
package com.example.anumatidemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.digitalanumati.consent.ConsentError
import com.digitalanumati.consent.ConsentErrorCode
import com.digitalanumati.consent.ConsentListener
import com.digitalanumati.consent.ConsentResult
import com.digitalanumati.consent.ConsentStatus
import com.digitalanumati.consent.DigitalAnumati
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private var consentStatus by mutableStateOf(ConsentStatus.NOT_DETERMINED)
private var errorMessage by mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (DigitalAnumati.isInitialized()) {
consentStatus = DigitalAnumati.getConsentStatus()
// First launch (or after a revoke): ask before continuing.
if (consentStatus == ConsentStatus.NOT_DETERMINED) showConsent()
}
setContent {
MaterialTheme {
HomeScreen(
sdkReady = DigitalAnumati.isInitialized(),
status = consentStatus,
error = errorMessage,
onManageConsent = ::showConsent,
onRevokeConsent = ::revokeConsent,
)
}
}
}
private fun showConsent() {
DigitalAnumati.showConsent(
activity = this,
listener = object : ConsentListener {
override fun onConsentCompleted(result: ConsentResult) {
consentStatus = result.status
errorMessage = null
}
override fun onError(error: ConsentError) {
errorMessage = when (error.code) {
ConsentErrorCode.USER_CANCELLED -> null // closed without choosing; keep the previous state
ConsentErrorCode.NETWORK_ERROR -> "Couldn't reach the consent service. Check your connection and try again."
else -> error.message
}
}
},
)
}
private fun revokeConsent() {
lifecycleScope.launch {
try {
DigitalAnumati.revokeConsent()
consentStatus = DigitalAnumati.getConsentStatus() // NOT_DETERMINED after a revoke
errorMessage = null
} catch (error: ConsentError) {
errorMessage = error.message
}
}
}
}
@Composable
fun HomeScreen(
sdkReady: Boolean,
status: ConsentStatus,
error: String?,
onManageConsent: () -> Unit,
onRevokeConsent: () -> Unit,
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(24.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("AnumatiDemoApp", style = MaterialTheme.typography.headlineMedium)
Text("SDK initialized: " + if (sdkReady) "Yes" else "No")
Text("Consent status: " + status.name)
if (error != null) {
Text(error, color = MaterialTheme.colorScheme.error)
}
Button(onClick = onManageConsent, enabled = sdkReady) {
Text("Manage Consent")
}
OutlinedButton(
onClick = onRevokeConsent,
enabled = sdkReady && status != ConsentStatus.NOT_DETERMINED,
) {
Text("Revoke Consent")
}
}
}
}

For release builds with R8 enabled, keep the SDK's public API:

app/proguard-rules.pro
# app/proguard-rules.pro
# A real SDK normally ships these as consumer rules inside its AAR; they're shown
# here so release (R8) builds of the demo keep the SDK's public API intact.
-keep class com.digitalanumati.consent.** { *; }
-keepclassmembers enum com.digitalanumati.consent.** { *; }
-dontwarn com.digitalanumati.consent.**

SDK types used above, for reference:

com.digitalanumati.consent (SDK public API)
// Public types exposed by the DEMO SDK (package com.digitalanumati.consent).
// Shown for reference — you don't write these; they ship inside the SDK.
// The Flutter plugin and React Native module call exactly this API.
package com.digitalanumati.consent
enum class Environment { UAT, PRODUCTION }
enum class ConsentStatus {
NOT_DETERMINED, // the user hasn't answered yet (or consent was revoked)
GRANTED, // every optional category accepted
DENIED, // every optional category rejected
PARTIAL, // some optional categories accepted
}
enum class ConsentCategory { NECESSARY, ANALYTICS, PERSONALIZATION, ADVERTISING }
data class ConsentPreferences(
val analytics: Boolean = false,
val personalization: Boolean = false,
val advertising: Boolean = false,
) {
val necessary: Boolean get() = true // always on; not user-editable
}
data class ConsentResult(
val status: ConsentStatus,
val preferences: ConsentPreferences,
val timestamp: Long, // epoch millis
)
enum class ConsentErrorCode {
INITIALIZATION_FAILED, // initialize() failed, or a call was made before initialize()
INVALID_CONFIGURATION,
INVALID_SITE_KEY,
INVALID_APPLICATION_ID,
NETWORK_ERROR,
CONSENT_LOAD_FAILED,
CONSENT_SAVE_FAILED,
USER_CANCELLED,
UNKNOWN_ERROR,
}
class ConsentError(
val code: ConsentErrorCode,
message: String,
cause: Throwable? = null,
) : Exception(message, cause)
interface ConsentListener {
fun onConsentCompleted(result: ConsentResult)
fun onError(error: ConsentError)
}
enum class ConsentEventType {
SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED,
CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR,
}
data class ConsentEvent(
val type: ConsentEventType,
val result: ConsentResult? = null, // set for ACCEPTED, REJECTED and UPDATED
val error: ConsentError? = null, // set for CONSENT_ERROR
)
object DigitalAnumati {
// Throws ConsentError: INVALID_CONFIGURATION, INVALID_SITE_KEY, INVALID_APPLICATION_ID, INITIALIZATION_FAILED
fun initialize(context: Context, siteKey: String, applicationId: String, environment: Environment)
fun isInitialized(): Boolean
// Presents the SDK-owned consent screen; the result or error arrives through the listener.
fun showConsent(activity: Activity, listener: ConsentListener)
fun getConsentStatus(): ConsentStatus // reads local storage, no network call
fun getConsentPreferences(): ConsentPreferences // reads local storage, no network call
suspend fun updateConsent(preferences: ConsentPreferences): ConsentResult
suspend fun revokeConsent()
// One listener; events are delivered on the main thread.
fun setEventListener(listener: ((ConsentEvent) -> Unit)?)
}

Folder structure of the sample app:

AnumatiDemoApp (Android)
AnumatiDemoApp/
├── app/
│ ├── src/main/
│ │ ├── java/com/example/anumatidemo/
│ │ │ ├── AnumatiDemoApplication.kt # SDK initialization
│ │ │ └── MainActivity.kt # Home screen, consent status, manage / revoke
│ │ └── AndroidManifest.xml
│ ├── build.gradle.kts # SDK dependency + BuildConfig values
│ └── proguard-rules.pro
├── build.gradle.kts
└── settings.gradle.kts

API reference

MethodParametersReturnsPurposePossible errors
initialize()context, siteKey, applicationId, environmentUnitConfigures the SDK. Call once in Application.onCreate().INVALID_CONFIGURATION, INVALID_SITE_KEY, INVALID_APPLICATION_ID, INITIALIZATION_FAILED
isInitialized()—BooleanWhether initialize() succeeded; use it to enable consent actions.—
showConsent()activity, listener: ConsentListenerUnit (result via listener)Opens the SDK-owned consent screen; delivers a ConsentResult to onConsentCompleted.INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED, USER_CANCELLED
getConsentStatus()—ConsentStatusReads the stored status without any network call.INITIALIZATION_FAILED, CONSENT_LOAD_FAILED
getConsentPreferences()—ConsentPreferencesReads the stored per-category choices without any network call.INITIALIZATION_FAILED, CONSENT_LOAD_FAILED
updateConsent()preferences: ConsentPreferences (suspend)ConsentResultSaves new choices without showing the screen, e.g. from your own settings toggle.INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED
revokeConsent()— (suspend)UnitWithdraws consent; status returns to NOT_DETERMINED.INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED
setEventListener()listener: ((ConsentEvent) -> Unit)?UnitReceives SDK events on the main thread; used by the Flutter and React Native bridges.—
Example
if (DigitalAnumati.isInitialized() &&
DigitalAnumati.getConsentStatus() == ConsentStatus.NOT_DETERMINED) {
DigitalAnumati.showConsent(activity = this, listener = consentListener)
}

iOS SDK (demo)

DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK

The package names, APIs, and endpoints in this section are placeholders that show how the integration is intended to work. They are not published, so the code won't resolve until the real SDK is released — the actual coordinates and endpoints will come from Digital Anumati.

Requirements: Xcode, Swift, iOS 15 or later. The SDK offers async/await APIs and a completion-handler variant for UIKit code. The examples below make up the iOS version of AnumatiDemoApp.

Installation

Swift Package Manager in Xcode: choose File → Add Package Dependencies…, enter https://github.com/example/digital-anumati-ios-sdk, select version 1.0.0 (“Up to Next Major”), and add the DigitalAnumatiConsent product to your app target. If your code lives in a local Swift package, declare it in Package.swift instead:

Package.swift
// swift-tools-version:5.9
// Package.swift — for teams that keep app code in a local Swift package.
// Most apps instead add the SDK in Xcode (see "Swift Package Manager in Xcode").
import PackageDescription
let package = Package(
name: "AnumatiDemoApp",
platforms: [.iOS(.v15)],
products: [
.library(name: "AnumatiDemoApp", targets: ["AnumatiDemoApp"]),
],
dependencies: [
// DEMO / FICTIONAL package URL
.package(url: "https://github.com/example/digital-anumati-ios-sdk", from: "1.0.0"),
],
targets: [
.target(
name: "AnumatiDemoApp",
dependencies: [
.product(name: "DigitalAnumatiConsent", package: "digital-anumati-ios-sdk"),
],
path: "AnumatiDemoApp"
),
]
)

Or, with CocoaPods:

Podfile
# Podfile — optional, if your project uses CocoaPods instead of SPM
platform :ios, '15.0'
target 'AnumatiDemoApp' do
use_frameworks!
# DEMO / FICTIONAL pod
pod 'DigitalAnumatiConsent', '~> 1.0'
end

Configuration & initialization

Keep per-environment values in .xcconfig files and expose them through Info.plist, so switching between UAT and production is a build-configuration change rather than a code change.

Config/UAT.xcconfig
// Config/UAT.xcconfig — one file per environment, selected by build configuration.
// Keep production values out of source control; inject them from CI.
ANUMATI_SITE_KEY = demo-site-key-123
ANUMATI_APPLICATION_ID = demo-mobile-app-123
ANUMATI_ENVIRONMENT = uat
Info.plist
<!-- Info.plist — values come from the active .xcconfig -->
<key>DigitalAnumatiSiteKey</key>
<string>$(ANUMATI_SITE_KEY)</string>
<key>DigitalAnumatiApplicationId</key>
<string>$(ANUMATI_APPLICATION_ID)</string>
<key>DigitalAnumatiEnvironment</key>
<string>$(ANUMATI_ENVIRONMENT)</string>
Models/SDKConfiguration.swift
// AnumatiDemoApp/Models/SDKConfiguration.swift
import Foundation
import DigitalAnumatiConsent
struct SDKConfiguration {
let siteKey: String
let applicationId: String
let environment: DigitalAnumati.Environment
enum ConfigurationError: LocalizedError {
case missingKey(String)
var errorDescription: String? {
switch self {
case .missingKey(let key): return "Missing \(key) in Info.plist."
}
}
}
static func fromInfoPlist(_ bundle: Bundle = .main) throws -> SDKConfiguration {
func value(_ key: String) throws -> String {
guard let value = bundle.object(forInfoDictionaryKey: key) as? String, !value.isEmpty else {
throw ConfigurationError.missingKey(key)
}
return value
}
return SDKConfiguration(
siteKey: try value("DigitalAnumatiSiteKey"),
applicationId: try value("DigitalAnumatiApplicationId"),
environment: try value("DigitalAnumatiEnvironment") == "production" ? .production : .uat
)
}
}

The app creates a ConsentService at launch, which initializes the SDK and publishes its state to the UI.

App/AnumatiDemoApp.swift
// AnumatiDemoApp/App/AnumatiDemoApp.swift
import SwiftUI
@main
struct AnumatiDemoApp: App {
// Creating the service initializes the SDK once, at launch.
@StateObject private var consent = ConsentService()
var body: some Scene {
WindowGroup {
HomeView()
.environmentObject(consent)
}
}
}
Services/ConsentService.swift
// AnumatiDemoApp/Services/ConsentService.swift
import UIKit
import DigitalAnumatiConsent
@MainActor
final class ConsentService: ObservableObject {
@Published private(set) var isInitialized = false
@Published private(set) var status: ConsentStatus = .notDetermined
@Published var errorMessage: String?
init() {
do {
let config = try SDKConfiguration.fromInfoPlist()
try DigitalAnumati.initialize(
siteKey: config.siteKey,
applicationId: config.applicationId,
environment: config.environment
)
isInitialized = true
status = DigitalAnumati.getConsentStatus()
} catch {
// Keep the app usable. Consent actions stay disabled until initialization succeeds.
errorMessage = "Consent SDK failed to initialize: \(error.localizedDescription)"
}
}
/// Presents the SDK's consent screen and records the result.
func showConsent() async {
guard let presenter = UIApplication.shared.topViewController else { return }
do {
let result = try await DigitalAnumati.showConsent(from: presenter)
status = result.status
errorMessage = nil
} catch let error as ConsentError where error.code == .userCancelled {
// Closed without choosing; keep the previous state.
} catch {
errorMessage = message(for: error)
}
}
func revokeConsent() async {
do {
try await DigitalAnumati.revokeConsent()
status = DigitalAnumati.getConsentStatus() // .notDetermined after a revoke
errorMessage = nil
} catch {
errorMessage = message(for: error)
}
}
private func message(for error: Error) -> String {
switch (error as? ConsentError)?.code {
case .networkError?:
return "Couldn't reach the consent service. Check your connection and try again."
case .initializationFailed?:
return "The consent SDK isn't initialized."
default:
return error.localizedDescription
}
}
}
Services/UIApplication+TopViewController.swift
// AnumatiDemoApp/Services/UIApplication+TopViewController.swift
import UIKit
extension UIApplication {
/// The view controller currently on screen — used to present the SDK's consent screen from SwiftUI.
var topViewController: UIViewController? {
let scene = connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }
var top = scene?.windows.first { $0.isKeyWindow }?.rootViewController
while let presented = top?.presentedViewController {
top = presented
}
return top
}
}

SwiftUI: the home screen shows the initialization state and consent status, and asks for consent on first launch.

Views/HomeView.swift
// AnumatiDemoApp/Views/HomeView.swift
import SwiftUI
import DigitalAnumatiConsent
struct HomeView: View {
@EnvironmentObject private var consent: ConsentService
var body: some View {
NavigationView {
List {
Section("SDK") {
row("Initialized", consent.isInitialized ? "Yes" : "No")
row("Consent status", consent.status.rawValue)
}
if let message = consent.errorMessage {
Section {
Text(message).foregroundStyle(.red)
}
}
Section {
Button("Manage Consent") {
Task { await consent.showConsent() }
}
Button("Revoke Consent", role: .destructive) {
Task { await consent.revokeConsent() }
}
.disabled(consent.status == .notDetermined)
}
.disabled(!consent.isInitialized)
}
.navigationTitle("AnumatiDemoApp")
.task {
// First launch (or after a revoke): ask before continuing.
if consent.isInitialized && consent.status == .notDetermined {
await consent.showConsent()
}
}
}
}
private func row(_ title: String, _ value: String) -> some View {
HStack {
Text(title)
Spacer()
Text(value).foregroundStyle(.secondary)
}
}
}

UIKit: use the completion-handler variant, which delivers a Result.

Views/PrivacySettingsViewController.swift
// UIKit — completion-handler API
import UIKit
import DigitalAnumatiConsent
final class PrivacySettingsViewController: UIViewController {
private let statusLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let manageButton = UIButton(type: .system)
manageButton.setTitle("Manage Consent", for: .normal)
manageButton.addTarget(self, action: #selector(manageConsent), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [statusLabel, manageButton])
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
statusLabel.text = "Consent status: \(DigitalAnumati.getConsentStatus().rawValue)"
}
@objc private func manageConsent() {
DigitalAnumati.showConsent(from: self) { [weak self] result in
switch result {
case .success(let consent):
self?.statusLabel.text = "Consent status: \(consent.status.rawValue)"
case .failure(let error) where error.code == .userCancelled:
break // closed without choosing
case .failure(let error):
self?.statusLabel.text = "Couldn't update consent: \(error.message)"
}
}
}
}

SDK types used above, for reference:

DigitalAnumatiConsent (SDK public API)
// Public types exposed by the DEMO SDK (module DigitalAnumatiConsent).
// Shown for reference — you don't write these; they ship inside the SDK.
// The Flutter plugin and React Native module call exactly this API.
import UIKit
public enum ConsentStatus: String {
case notDetermined = "NOT_DETERMINED" // no answer yet (or consent was revoked)
case granted = "GRANTED" // every optional category accepted
case denied = "DENIED" // every optional category rejected
case partial = "PARTIAL" // some optional categories accepted
}
public enum ConsentCategory: String, CaseIterable {
case necessary = "NECESSARY", analytics = "ANALYTICS"
case personalization = "PERSONALIZATION", advertising = "ADVERTISING"
}
public struct ConsentPreferences: Equatable {
public var necessary: Bool { true } // always on; not user-editable
public var analytics: Bool
public var personalization: Bool
public var advertising: Bool
public init(analytics: Bool = false, personalization: Bool = false, advertising: Bool = false)
}
public struct ConsentResult {
public let status: ConsentStatus
public let preferences: ConsentPreferences
public let timestamp: Date
}
public struct ConsentError: LocalizedError {
public enum Code: String {
case initializationFailed = "INITIALIZATION_FAILED" // also: called before initialize()
case invalidConfiguration = "INVALID_CONFIGURATION"
case invalidSiteKey = "INVALID_SITE_KEY"
case invalidApplicationId = "INVALID_APPLICATION_ID"
case networkError = "NETWORK_ERROR"
case consentLoadFailed = "CONSENT_LOAD_FAILED"
case consentSaveFailed = "CONSENT_SAVE_FAILED"
case userCancelled = "USER_CANCELLED"
case unknownError = "UNKNOWN_ERROR"
}
public let code: Code
public let message: String
public var errorDescription: String? { message }
}
public enum ConsentEventType: String {
case sdkInitialized = "SDK_INITIALIZED", consentShown = "CONSENT_SHOWN"
case consentAccepted = "CONSENT_ACCEPTED", consentRejected = "CONSENT_REJECTED"
case consentUpdated = "CONSENT_UPDATED", consentRevoked = "CONSENT_REVOKED"
case consentError = "CONSENT_ERROR"
}
public struct ConsentEvent {
public let type: ConsentEventType
public let result: ConsentResult? // set for accepted, rejected and updated
public let error: ConsentError? // set for consentError
}
public enum DigitalAnumati {
// Nested so it doesn't clash with SwiftUI's @Environment property wrapper.
public enum Environment { case uat, production }
// Throws ConsentError: invalidConfiguration, invalidSiteKey, invalidApplicationId, initializationFailed
public static func initialize(siteKey: String, applicationId: String, environment: Environment) throws
public static var isInitialized: Bool { get }
// Presents the SDK-owned consent screen.
@MainActor
public static func showConsent(
from viewController: UIViewController,
completion: @escaping (Result<ConsentResult, ConsentError>) -> Void
)
@MainActor
public static func showConsent(from viewController: UIViewController) async throws -> ConsentResult
public static func getConsentStatus() -> ConsentStatus // local storage, no network call
public static func getConsentPreferences() -> ConsentPreferences // local storage, no network call
public static func updateConsent(_ preferences: ConsentPreferences) async throws -> ConsentResult
public static func revokeConsent() async throws
// One handler; events are delivered on the main thread.
public static func setEventHandler(_ handler: ((ConsentEvent) -> Void)?)
}

Folder structure of the sample app:

AnumatiDemoApp (iOS)
AnumatiDemoApp/
├── AnumatiDemoApp/
│ ├── App/
│ │ └── AnumatiDemoApp.swift # @main, creates ConsentService
│ ├── Views/
│ │ ├── HomeView.swift # status, Manage / Revoke buttons
│ │ └── PrivacySettingsViewController.swift # UIKit example
│ ├── Services/
│ │ ├── ConsentService.swift # SDK init, show / revoke, errors
│ │ └── UIApplication+TopViewController.swift
│ ├── Models/
│ │ └── SDKConfiguration.swift # reads Info.plist values
│ └── Info.plist
├── Config/
│ ├── UAT.xcconfig
│ └── Production.xcconfig
└── Package.swift

API reference

MethodParametersReturnsPurposePossible errors
initialize(siteKey:applicationId:environment:)String, String, DigitalAnumati.EnvironmentVoid (throws)Configures the SDK. Call once at launch..invalidConfiguration, .invalidSiteKey, .invalidApplicationId, .initializationFailed
isInitialized— (static property)BoolWhether initialization succeeded.—
showConsent(from:) asyncUIViewControllerConsentResult (throws)Presents the SDK-owned consent screen and returns the user's choice..initializationFailed, .networkError, .consentSaveFailed, .userCancelled
showConsent(from:completion:)UIViewController, (Result<ConsentResult, ConsentError>) -> VoidVoidCompletion-handler variant for UIKit.Same as above, via .failure
getConsentStatus()—ConsentStatusReads the stored status without any network call.— (returns .notDetermined before initialization)
getConsentPreferences()—ConsentPreferencesReads the stored per-category choices without any network call.— (returns defaults before initialization)
updateConsent(_:) asyncConsentPreferencesConsentResult (throws)Saves new choices without showing the screen..initializationFailed, .networkError, .consentSaveFailed
revokeConsent() async—Void (throws)Withdraws consent; status returns to .notDetermined..initializationFailed, .networkError, .consentSaveFailed
setEventHandler(_:)((ConsentEvent) -> Void)?VoidReceives SDK events on the main thread; used by the Flutter and React Native bridges.—
Example
if DigitalAnumati.getConsentStatus() == .notDetermined {
let result = try await DigitalAnumati.showConsent(from: viewController)
print("Consent status: \(result.status)")
}

Cross-platform SDKs (demo)

DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK

The package names, APIs, and endpoints in this section are placeholders that show how the integration is intended to work. They are not published, so the code won't resolve until the real SDK is released — the actual coordinates and endpoints will come from Digital Anumati.

Flutter and React Native apps use the same native SDKs through thin bridges. The bridges only handle serialization, method calls, callbacks, events, error mapping, and type conversion; consent storage, rules, validation, backend communication, and the consent screen stay in the native SDK, so every app behaves the same way.

Architecture
Consent Backend
|
+--------------+--------------+
| |
Android Native SDK iOS Native SDK
(consent logic, storage, (consent logic, storage,
consent UI, networking) consent UI, networking)
| |
+-------+-------+ +-------+-------+
| | | |
Flutter React Native Flutter React Native
plugin native module plugin native module
(Kotlin) (Kotlin) (Swift) (Swift)
| | | |
+-------+-------+-------------+-------+-------+
| |
Dart API (Flutter) TypeScript API (React Native)
| |
+--------- Host application --+

Data type mapping

ConceptAndroidiOSFlutterReact Native
ConsentStatusKotlin enumSwift enum (String raw values)Dart enum (wire value)TypeScript string union
ConsentCategoryKotlin enumSwift enumDart enumTypeScript string union
ConsentPreferencesKotlin data classSwift structDart classTypeScript interface
ConsentResultKotlin data class (timestamp: Long ms)Swift struct (timestamp: Date)Dart class (DateTime)TypeScript interface (timestamp: number ms)
ConsentErrorKotlin exception with codeSwift Error with codeDart exception (ConsentError)JS Error with code
EnvironmentEnvironment.UAT / PRODUCTIONDigitalAnumati.Environment .uat / .productionEnvironment.uat / .production'uat' | 'production'
EventssetEventListener callbacksetEventHandler closureStream<ConsentEvent> (EventChannel)addListener (NativeEventEmitter)
Async callssuspend functions / listenerasync/await / completion handlerFuturePromise

Error contract

Every platform reports the same code strings. Flutter receives them as ConsentError.code (from PlatformException); React Native as error.code on the rejected Promise.

CodeMeaningTypical cause
INITIALIZATION_FAILEDThe SDK isn't readyinitialize() failed, or a call ran before it completed
INVALID_CONFIGURATIONConfiguration is incomplete or inconsistentMissing values or an unknown environment
INVALID_SITE_KEYThe site key was rejectedWrong key, or a key from the other environment
INVALID_APPLICATION_IDThe application ID was rejectedWrong ID, or an ID from the other environment
NETWORK_ERRORThe consent service couldn't be reachedNo connectivity or a TLS failure
CONSENT_LOAD_FAILEDStored consent couldn't be readCorrupted or inaccessible local storage
CONSENT_SAVE_FAILEDConsent couldn't be savedStorage or service write failure
USER_CANCELLEDThe user closed the consent screen without choosingNot a failure — keep the previous state
UNKNOWN_ERRORAnything elseUnexpected native exception; no presenting activity / view controller

Events (SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR) use the same names everywhere — see the event contract.

Both mobile SDKs present the same screen. Necessary is always on; the optional categories can be toggled. Accept All and Reject All submit immediately; Save Preferences submits the current toggles. Try it:

Privacy & Consent

AnumatiDemoApp asks for your permission to process some of your data. Choose what you're comfortable with — you can change this any time in Settings.

  • Necessary

    Needed for sign-in, security and core features. Always on.

  • Analytics

    Helps us understand how the app is used so we can improve it.

  • Personalization

    Remembers your preferences to tailor content.

  • Advertising

    Shows ads that are more relevant to you.

ConsentResult returned to the app

Toggle categories and press a button to see the result the host app receives in onConsentCompleted (Android), the completion handler (iOS), or the resolved showConsent() call (Flutter and React Native).

StatusWhen
GRANTED / .grantedEvery optional category accepted
DENIED / .deniedEvery optional category rejected
PARTIAL / .partialSome optional categories accepted
NOT_DETERMINED / .notDeterminedNo answer yet, or consent was revoked

Consent lifecycle

Launch flow
Application starts
↓
SDK initialization
↓
Check existing consent (getConsentStatus)
↓
If consent exists ──────────────→ Continue application
↓
If consent does not exist
↓
Display consent UI (showConsent)
↓
User selects preferences
↓
SDK stores consent
↓
Return ConsentResult
↓
Application continues
ScenarioWhat happens
Changing consentThe user taps Manage Consent → showConsent() opens with their current choices pre-selected → the new ConsentResult replaces the stored one.
Revoking consentrevokeConsent() withdraws consent on the service and clears the stored result → status becomes NOT_DETERMINED → the app asks again on next launch.
Application restartinitialize() restores the stored result, so getConsentStatus() returns the last decision immediately — no network call, and no consent screen if a decision exists.
Initialization failureinitialize() throws (e.g. missing key) → isInitialized is false → the app keeps running with consent actions disabled and treats optional processing as not consented.

UAT & production

EnvironmentAndroidiOSFlutterReact NativeBase URL (placeholder)
UATEnvironment.UAT.uatEnvironment.uat'uat'https://api.uat.example.digitalanumati.com
ProductionEnvironment.PRODUCTION.productionEnvironment.production'production'https://api.example.digitalanumati.com

Demo configuration: site key demo-site-key-123, application ID demo-mobile-app-123, environment UAT. These URLs are fictional placeholders — the SDK selects the endpoint from the environment, and real production endpoints must be supplied by the actual SDK provider. Select the environment per build (debug → UAT, release → production) rather than switching it in code — in Flutter with kReleaseMode, in React Native with __DEV__.

Security

  • Don't hard-code production values in source; inject them per build from CI.
  • Treat site keys and application IDs according to the actual SDK provider's security model — they identify your app, they aren't secrets.
  • Never put private API credentials (such as the server x-api-key) in a mobile app. Server-side verification belongs on your backend.
  • All SDK traffic must use HTTPS.
  • Don't log consent data or identifiers unnecessarily, especially in release builds.
  • Protect configuration files and keep production xcconfig / Gradle properties out of source control.
  • Handle SDK and network errors gracefully — the app should keep working if consent can't sync.
  • Never put private server credentials in Flutter or React Native code either — bundled Dart and JavaScript are as easy to extract as native code.
  • Consent is stored locally by the native SDK in protected storage (Android Keystore-backed encrypted storage, iOS Keychain); the Flutter and React Native layers never persist it.
  • Keep the platform's certificate validation on: don't disable App Transport Security or allow cleartext traffic, and don't add custom trust managers.
  • Treat reference IDs, emails, and phone numbers as PII — don't send them to analytics or logs.

Testing checklist

  • Fresh install shows the consent screen; a second launch doesn't.
  • Accept All, Reject All, and Save Preferences return GRANTED, DENIED, and PARTIAL respectively.
  • Closing the screen without choosing keeps the previous status (USER_CANCELLED).
  • Revoke Consent resets the status to NOT_DETERMINED and the screen appears on next launch.
  • Airplane mode: showConsent / revokeConsent report a network error and the app stays usable.
  • Missing or empty site key: initialization fails gracefully and consent actions are disabled.
  • Release build with R8 (Android) still initializes and shows the consent screen.
  • UAT and production builds point at the right environment.

Troubleshooting

Web SDK doesn't load

Verify the environment configuration, domain setup, and that the script loads before user interaction.
SymptomLikely causeFix
INITIALIZATION_FAILEDA call ran before initialize(), or initialize() failedInitialize in Application.onCreate() / at app launch and check isInitialized first.
INVALID_CONFIGURATION / INVALID_SITE_KEY at launchMissing, empty or wrong site key or application IDCheck BuildConfig fields (Android) or the Info.plist / xcconfig values (iOS).
Network error on showConsentNo connectivity, or wrong environmentRetry when online; confirm UAT vs production for the build.
Consent screen never appearsA decision is already storedExpected — call showConsent() from a Manage Consent action, or revoke to reset.
Crash or missing classes in release (Android)R8 removed SDK classesAdd the ProGuard rules above.
Package resolution failsThe demo SDK isn't publishedExpected for this demo — use the real coordinates once the SDK is released.