Consent management (CMP)
Ezoic's mobile app SDKs include a consent management platform (CMP). It asks users for the consent that privacy laws require and stores their choices in the standard IAB keys that Google Mobile Ads, Prebid and other ad SDKs read. Your Ezoic settings for your domain decide what it shows.
Apps that show Ezoic ads get the CMP through the ads SDK, with nothing extra to install. From SDK 1.22.0, Ezoic publishers whose app doesn't use Ezoic ads can install the CMP on its own (see Use the CMP without Ezoic ads).
What the CMP does
What the user sees depends on where they are:
- GDPR regions: an IAB TCF 2.4 consent dialog (CMP ID 299). The decision is stored as the standard
IABTCF_*keys, and ad loads wait until the user decides. - Covered US states: a privacy notice and a "Do Not Sell My Information" choice, stored as the standard IAB GPP keys (
IABGPP_*) for the user's state. Ads never wait for it. See US-state privacy. - Everywhere else: nothing is shown, and ads load as usual.
The keys are written to the app's default SharedPreferences on Android and to UserDefaults.standard on iOS.
Use the CMP with Ezoic ads
If your app uses an Ezoic ads SDK, the CMP is already included and on by default (cmpEnabled is true). You don't install anything else. Use the consent methods on EzoicAds:
presentConsentIfRequiredshows the GDPR dialog when the user must decide. Call it once per launch, right afterinitialize. The Flutter, React Native and Capacitor SDKs call it for you afterinitializeunless you setautoPresentConsenttofalse.presentConsentSettingsreopens the user's privacy choices. Call it from your "Privacy choices" entry point (see Add a "Privacy choices" entry point).isPrivacyOptionsRequiredtells you whether to show that entry point.isConsentRequiredtells you whether GDPR applies and the built-in CMP is in charge.resetConsentdeletes the stored decision and the US choice, so the dialog or US notice shows again.
Each platform guide covers its version of these calls, outcomes and options:
US-state privacy
From SDK 1.22.0, the CMP handles the US states with consumer privacy laws the same way Ezoic's web CMP does. The covered states are California, Colorado, Connecticut, Delaware, Florida, Iowa, Montana, Nebraska, New Hampshire, New Jersey, Oregon, Tennessee, Texas, Utah and Virginia.
There is nothing to call and no new setting. It runs whenever the built-in CMP is on, and cmpEnabled: false turns it off along with the GDPR dialog.
- Notice. At launch, a card slides up from the bottom of the screen with the heading "Privacy Preferences" and two buttons, Consent and Do Not Sell My Information. The card has no backdrop, so the rest of your app stays usable. Only a choice dismisses it. Until then it follows the user from screen to screen, and it never sits over a full-screen ad or one of the SDK's own screens. While it is up, Ezoic anchor ads are hidden. If the user leaves the app without choosing, the card shows again at the next launch. After a choice it isn't shown again, unless the choice is cleared with
resetConsentor the user moves to a different covered state. The notice is in English. - Do Not Sell. Do Not Sell My Information opens the US privacy settings: a full screen with one "Do not sell or share my information" toggle and a Save Settings button.
presentConsentSettingsopens the same screen. - Signals. The CMP writes the GPP section for the user's state. Until the user opts out, the keys say "not opted out", with the notice given. After an opt-out, the keys say so, and Google Ad Manager requests ask for restricted data processing.
- Ads aren't blocked. US privacy laws are opt-out, so ad loads never wait for the notice or the user's choice.
presentConsentIfRequiredis about GDPR only. In a covered US state it returnsNotRequired, because the CMP shows the US notice by itself.- Hands off. The CMP writes no
IABGPP_*keys and shows no US notice when another CMP has already writtenIABGPP_*keys, when your app passes GPP itself withsetGPPConsent, or whenautoReadConsentisfalse. When the user is no longer in a covered state, it removes only the keys it wrote.
Add a "Privacy choices" entry point
Users must be able to change their choice at any time. TCF policy requires a way to reopen the dialog and withdraw consent, and US users need a way to opt out. Add a button or menu item, for example "Privacy choices" in your settings screen, that calls presentConsentSettings.
isPrivacyOptionsRequired (SDK 1.22.0 and later) tells you whether that entry point is needed. It works like Google UMP's privacyOptionsRequirementStatus:
| Value | Meaning |
|---|---|
true |
The user is in a GDPR region or a covered US state, and the built-in CMP handles consent |
false |
No entry point is needed: the user is elsewhere, the built-in CMP is off, or another CMP or your app manages consent |
null / nil |
Not known yet. The SDK hasn't finished loading your settings |
Show the entry point unless the value is false. A null value means "not known yet", so don't hide the entry point because of it. Check the value again later, for example each time the screen with the entry point appears. Tapping the entry point when there is nothing to show is harmless: presentConsentSettings returns NotRequired.
// Android (Kotlin)
privacyButton.isVisible = EzoicAds.instance.isPrivacyOptionsRequired != false
privacyButton.setOnClickListener {
EzoicAds.instance.presentConsentSettings(this) { outcome -> /* ... */ }
}
// iOS
privacyButton.isHidden = EzoicAds.shared.isPrivacyOptionsRequired == false
@objc func privacyChoicesTapped() {
EzoicAds.shared.presentConsentSettings(from: self) { outcome in /* ... */ }
}
| Platform | With Ezoic ads | Standalone CMP |
|---|---|---|
| Android (Kotlin) | EzoicAds.instance.isPrivacyOptionsRequired |
EzoicCmp.isPrivacyOptionsRequired |
| Android (Java) | EzoicAds.getInstance().isPrivacyOptionsRequired() |
EzoicCmp.isPrivacyOptionsRequired() |
| iOS | EzoicAds.shared.isPrivacyOptionsRequired |
EzoicCmp.isPrivacyOptionsRequired |
| Flutter | EzoicAds.isPrivacyOptionsRequired() |
EzoicCmp.isPrivacyOptionsRequired() |
| React Native | EzoicAds.isPrivacyOptionsRequired() |
EzoicCmp.isPrivacyOptionsRequired() |
| Capacitor | EzoicAds.isPrivacyOptionsRequired() |
EzoicCmp.isPrivacyOptionsRequired() |
| Unity | EzoicAds.IsPrivacyOptionsRequired |
EzoicCmp.IsPrivacyOptionsRequired |
The Flutter, React Native and Capacitor calls return a future or promise.
Don't decide on isConsentRequired instead. It answers only whether GDPR applies, so it stays false for users in covered US states.
What presentConsentSettings opens
From SDK 1.22.0, presentConsentSettings opens the screen for where the user is, so an existing "Privacy settings" button works for US users without code changes:
| Where the user is | What opens | Outcomes |
|---|---|---|
| GDPR region | The TCF dialog, with the user's stored choices | As before: Decided(ACCEPT_ALL / REJECT_ALL / CUSTOM) or Dismissed. If the dialog can't load (for example offline) and a still-valid decision is stored, AlreadyDecided |
| Covered US state | The US privacy settings | Decided(ACCEPT_ALL): not opted out. Decided(REJECT_ALL): opted out of the sale or sharing of their information. Dismissed: closed without saving |
| Elsewhere, the CMP is off, or another CMP or your app manages consent | Nothing | NotRequired |
The outcome names follow each platform's style, for example .decided(.rejectAll) on iOS and { type: 'decided', decision: 'rejectAll' } in React Native and Capacitor. SDK 1.22.0 adds no new outcomes.
Use the CMP without Ezoic ads
From SDK 1.22.0, the CMP also ships on its own, for Ezoic publishers whose app doesn't use Ezoic ads. It gives the app the same GDPR and US-state handling it would get through EzoicAds, driven by the same Ezoic settings for your domain. The standalone entry point, EzoicCmp, has the same consent methods and outcomes as EzoicAds.
domain must be a domain in your Ezoic account, the same value the ads SDK takes. Ezoic refuses other domains, and the CMP then does nothing: every present call fails with code 1001 (not initialized), isConsentRequired and isPrivacyOptionsRequired stay null, no US notice is shown, and nothing is written.
EzoicAds and don't add the standalone package: the ads SDK already includes the CMP. If an app initializes both EzoicAds and EzoicCmp, in either order, EzoicAds takes over the same CMP, the stored decision carries over, and EzoicCmp.initialize is ignored. If both packages are installed, use the same version of each. Each native library is still linked once.
Install
The standalone CMP is version 1.22.0 on every platform. It needs the same OS versions as the ads SDKs: Android SDK 24 or higher, and iOS 15.0 or higher with Xcode 26.2 or higher.
Android. Add the dependency from Maven Central:
dependencies {
implementation("com.ezoic.sdk:ezoic-cmp-sdk:1.22.0")
}
iOS. Use Swift Package Manager. In Xcode, go to File > Add Package Dependencies, enter https://github.com/ezoic/ezoic-swift-sdk-dist.git with Up to Next Major Version from 1.22.0, and add the EzoicCmpSDK product (not EzoicAdsSDK) to your app target. Or in Package.swift:
dependencies: [
.package(url: "https://github.com/ezoic/ezoic-swift-sdk-dist.git", from: "1.22.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "EzoicCmpSDK", package: "ezoic-swift-sdk-dist")
]
)
]
Then write import EzoicCmpSDK. There is no CocoaPods pod for the native iOS CMP.
Flutter. Add ezoic_flutter_cmp_sdk from the Flutter SDK repository, pinned to the release tag:
dependencies:
ezoic_flutter_cmp_sdk:
git:
url: https://github.com/ezoic/flutter-sdk.git
ref: v1.22.0
path: ezoic_flutter_cmp_sdk
It works with Swift Package Manager and with CocoaPods. With CocoaPods, set platform :ios, '15.0' in ios/Podfile. pod install then downloads the native CMP framework from the SDK's GitHub release and checks its SHA-256, so the first pod install needs network access to GitHub.
React Native. React Native 0.76 or higher with the New Architecture:
npm install @ezoic/react-native-cmp-sdk@1.22.0
cd ios && pod install
The native iOS CMP ships inside the npm package, so no Podfile changes are needed.
Capacitor. Capacitor 7 or 8:
npm install @ezoic/capacitor-cmp-sdk@1.22.0
npx cap sync
With Swift Package Manager, npx cap sync adds the EzoicCmpSDK product. With CocoaPods, the plugin's pod uses the native CMP framework that ships in the npm package.
Unity. Unity 2021.3 LTS or higher. Open Window → Package Manager → + → Add package from git URL… and enter:
https://github.com/ezoic/ezoic-unity-sdk.git?path=/com.ezoic.cmp-sdk~#v1.22.0
Or add it to Packages/manifest.json:
{
"dependencies": {
"com.ezoic.cmp-sdk": "https://github.com/ezoic/ezoic-unity-sdk.git?path=/com.ezoic.cmp-sdk~#v1.22.0"
}
}
With the External Dependency Manager (Unity's EDM 2.1 or higher, or EDM4U 1.2.189 or higher), the Android Resolver adds com.ezoic.sdk:ezoic-cmp-sdk to your Gradle build. The iOS Resolver adds the ezoic-swift-sdk-dist Swift package with its EzoicCmpSDK product to UnityFramework. Keep Swift Package Manager Integration on in the iOS Resolver settings; there is no CocoaPod. Without EDM, add implementation 'com.ezoic.sdk:ezoic-cmp-sdk:1.22.0' to a custom main Gradle template, and add the Swift package's EzoicCmpSDK product to the UnityFramework target in Xcode.
Initialize and show the dialog
Initialize once, early, with your Ezoic domain. initialize returns at once and loads your settings in the background. Then present the dialog from your first screen. The call waits for the settings, and shows the dialog only where GDPR applies and the user hasn't decided. In covered US states, the CMP shows its US notice by itself.
Android (Kotlin):
import com.ezoic.ads.sdk.privacy.cmp.ConsentOutcome
import com.ezoic.ads.sdk.privacy.cmp.EzoicCmp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
EzoicCmp.initialize(this, "example.com")
}
}
// In your first Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EzoicCmp.presentConsentIfRequired(this) { outcome ->
when (outcome) {
is ConsentOutcome.Decided -> Log.d("Ezoic", "User chose ${outcome.decision}")
is ConsentOutcome.Failed -> Log.w("Ezoic", "Consent UI failed: ${outcome.error.message}")
else -> Unit // NotRequired, AlreadyDecided, Dismissed, AlreadyPresenting
}
}
}
// "Privacy choices" entry point
privacyButton.isVisible = EzoicCmp.isPrivacyOptionsRequired != false
privacyButton.setOnClickListener {
EzoicCmp.presentConsentSettings(this) { outcome -> /* ... */ }
}
Android (Java):
import com.ezoic.ads.sdk.privacy.cmp.ConsentOutcome;
import com.ezoic.ads.sdk.privacy.cmp.EzoicCmp;
// In Application.onCreate
EzoicCmp.initialize(this, "example.com");
// In your first Activity's onCreate
EzoicCmp.presentConsentIfRequiredWithListener(this, outcome -> {
if (outcome instanceof ConsentOutcome.Decided) {
Log.d("Ezoic", "User chose " + ((ConsentOutcome.Decided) outcome).getDecision());
}
});
// "Privacy choices" entry point
privacyButton.setVisibility(
Boolean.FALSE.equals(EzoicCmp.isPrivacyOptionsRequired()) ? View.GONE : View.VISIBLE);
privacyButton.setOnClickListener(v ->
EzoicCmp.presentConsentSettingsWithListener(this, outcome -> { /* ... */ }));
iOS:
import EzoicCmpSDK
// At app startup, for example in application(_:didFinishLaunchingWithOptions:)
EzoicCmp.initialize(domain: "example.com")
// In your first view controller
private static var consentRequested = false // once per process
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !Self.consentRequested else { return }
Self.consentRequested = true
EzoicCmp.presentConsentIfRequired(from: self) { outcome in
switch outcome {
case .decided(let decision):
print("User chose \(decision)")
case .failed(let error):
print("Consent UI failed: \(error.localizedDescription)")
default:
break // .notRequired, .alreadyDecided, .dismissed, .alreadyPresenting
}
}
}
// "Privacy choices" entry point
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
privacyButton.isHidden = EzoicCmp.isPrivacyOptionsRequired == false
}
@objc func privacyChoicesTapped() {
EzoicCmp.presentConsentSettings(from: self) { outcome in /* ... */ }
}
async/await is also supported: let outcome = await EzoicCmp.presentConsentIfRequired(from: self).
Flutter. The plugin presents the dialog for you once settings have loaded, unless you set autoPresentConsent: false:
import 'package:ezoic_flutter_cmp_sdk/ezoic_flutter_cmp_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
await EzoicCmp.initialize('example.com');
}
// "Privacy choices" entry point
FutureBuilder<bool?>(
future: EzoicCmp.isPrivacyOptionsRequired(),
builder: (context, snapshot) => snapshot.data != false
? TextButton(
onPressed: EzoicCmp.presentConsentSettings,
child: const Text('Privacy choices'),
)
: const SizedBox.shrink(),
)
To choose the timing yourself:
await EzoicCmp.initialize(
'example.com',
const EzoicCmpConfiguration(autoPresentConsent: false),
);
final outcome = await EzoicCmp.presentConsentIfRequired();
React Native:
import { EzoicCmp } from '@ezoic/react-native-cmp-sdk';
// Once, early in app startup
EzoicCmp.initialize('example.com');
// From your first screen
const outcome = await EzoicCmp.presentConsentIfRequired();
// "Privacy choices" entry point
const [showPrivacy, setShowPrivacy] = useState(true);
useEffect(() => {
EzoicCmp.isPrivacyOptionsRequired().then((v) => setShowPrivacy(v !== false));
}, []);
{showPrivacy && (
<Button
title="Privacy choices"
onPress={() => EzoicCmp.presentConsentSettings()}
/>
)}
Capacitor. The plugin presents the dialog for you once settings have loaded, unless you set autoPresentConsent: false:
import { EzoicCmp } from '@ezoic/capacitor-cmp-sdk';
await EzoicCmp.initialize({ domain: 'example.com' });
// "Privacy choices" entry point
const button = document.getElementById('privacy-choices')!;
button.hidden = (await EzoicCmp.isPrivacyOptionsRequired()) === false;
button.addEventListener('click', () => {
EzoicCmp.presentConsentSettings();
});
On the web, every call is a no-op: present calls resolve notRequired, and the checks resolve null.
Unity:
using Ezoic.Cmp;
using UnityEngine;
public class ConsentBootstrap : MonoBehaviour
{
void Start()
{
EzoicCmp.Initialize("example.com");
EzoicCmp.PresentConsentIfRequired(result =>
{
// result.Outcome: NotRequired, AlreadyDecided, Decided (see result.Decision),
// Dismissed (ask again later), AlreadyPresenting, or Failed (see result.Error).
Debug.Log("Consent: " + result);
});
}
// Show the "Privacy choices" button while this is true
bool ShowPrivacyButton => EzoicCmp.IsPrivacyOptionsRequired != false;
public void OnPrivacyChoices()
{
EzoicCmp.PresentConsentSettings(result => Debug.Log("Privacy settings: " + result));
}
}
Make the first call from the Unity main thread. Results arrive on the main thread. In the editor and on other platforms, calls are safe no-ops: present calls report Failed, and the checks return null.
Calling initialize again is harmless; repeat calls are ignored. Calling presentConsentIfRequired again is also harmless: you get AlreadyPresenting while a dialog is in flight, and AlreadyDecided once a decision is stored.
Options
Pass options as the configuration argument to initialize: EzoicCmpConfiguration on Android, iOS, Flutter and Unity, and an options object in React Native and Capacitor.
| Option | Default | Meaning |
|---|---|---|
cmpEnabled |
true |
Run the CMP. Set false if your app runs another CMP |
autoReadConsent |
true |
false means your app manages consent itself: the CMP never presents anything or writes keys |
debugEnabled |
false |
Also write the CMP's debug and info lines to the platform log (Logcat tag EzoicCmp; iOS subsystem com.ezoic.cmp.sdk) |
logger |
none | Android and iOS only: a CmpLogger that receives the CMP's log lines instead of the platform log |
autoPresentConsent |
true |
Flutter and Capacitor only: present the dialog once, if required, right after initialize |
Java apps build the configuration with new EzoicCmpConfiguration.Builder().debugEnabled(true).build(). Unity uses property names (CmpEnabled, AutoReadConsent, DebugEnabled).
Outcomes and errors
presentConsentIfRequired and presentConsentSettings return the same outcomes as on EzoicAds (see What presentConsentSettings opens and your platform guide). In the wrappers, they never throw or reject.
When the dialog can't be shown, the Failed outcome carries a CmpError on Android and iOS, and a numeric code in the wrappers:
CmpError |
code |
When |
|---|---|---|
NotInitialized |
1001 |
Called before initialize, settings never loaded, or Ezoic refused the domain. Nothing is written |
Network |
2001 |
The consent configuration or vendor list couldn't be loaded |
Render |
4001 |
The consent screen couldn't be shown |
InvalidConsent |
5002 |
The decision couldn't be encoded or saved |
The wrappers also use code: -1 when there was no foreground screen to present from. Call presentConsentIfRequired again once a screen is showing.
isConsentRequired is null before initialize, while your settings load, and when none could be loaded, for example because the domain was refused or the device is offline with nothing cached. If loading fails after an earlier launch succeeded, the CMP uses the settings it loaded last time.
Using your own CMP
If your app already runs another CMP, such as Google UMP or OneTrust, turn the built-in CMP off with cmpEnabled: false (Java: .cmpEnabled(false), Unity: CmpEnabled = false). If you don't use Ezoic ads, don't install the standalone CMP. The ads SDKs then read your CMP's keys as before:
- TCF consent from
IABTCF_*keys - GPP consent from
IABGPP_*keys - US Privacy consent from the standard IAB US Privacy key
The built-in CMP also stays out of the way by itself:
- It doesn't show the GDPR dialog when it finds
IABTCF_CmpSdkIDset to another CMP's ID. - It doesn't write
IABGPP_*keys or show the US notice when another CMP has already writtenIABGPP_*keys. - It stands down when your app passes consent with
setGDPRConsentorsetGPPConsent, or setsautoReadConsenttofalse.
Set cmpEnabled: false explicitly anyway, so the built-in CMP never acts before your own CMP has written its keys.
Upgrading to 1.22.0
Most apps need no code changes. Check these:
- US users now see the notice. If your app runs its own CMP for US states, make sure
cmpEnabledisfalse. If your "Privacy settings" button is shown only whileisConsentRequiredistrue, show it based onisPrivacyOptionsRequiredinstead, so that US users can reach it (see Add a "Privacy choices" entry point). - The CMP is its own library. On Android,
com.ezoic.sdk:ezoic-ads-sdknow depends oncom.ezoic.sdk:ezoic-cmp-sdk, and Gradle adds it for you. If your build excludes transitive dependencies or uses a dependency allow-list, allowcom.ezoic.sdk:ezoic-cmp-sdkat the same version. On iOS, theEzoicAdsSDKSwift package product brings in the CMP. Package and import names don't change:com.ezoic.ads.sdk.privacy.cmpon Android, andimport EzoicAdsSDKon iOS.
ConsentOutcome.Failed carries Exception / Error
This affects only native Kotlin, Java and Swift code that reads the error from a Failed outcome. The Flutter, React Native, Capacitor and Unity outcomes keep their shape and codes.
ConsentOutcome.Failed.error was an EzoicError. It is now an Exception on Android and an Error on iOS, because the same outcome type is shared with the standalone CMP. Through EzoicAds, the error is still an EzoicError at runtime, so is EzoicError.NotInitialized checks keep compiling. Through EzoicCmp, it is a CmpError. Reading code now needs a cast:
is ConsentOutcome.Failed -> {
val code = (outcome.error as? EzoicError)?.code // EzoicCmp: (outcome.error as? CmpError)?.code
Log.w("Ezoic", "Consent UI failed ($code): ${outcome.error.message}")
}
if (outcome instanceof ConsentOutcome.Failed) {
Exception error = ((ConsentOutcome.Failed) outcome).getError();
Integer code = error instanceof EzoicError ? ((EzoicError) error).getCode() : null;
}
case .failed(let error):
// EzoicError and CmpError both conform to EzoicErrorCode
let code = (error as? EzoicErrorCode)?.code
print("Consent UI failed (\(String(describing: code))): \(error.localizedDescription)")
The codes are unchanged: 1001, 2001, 4001 and 5002, as in Outcomes and errors.
iOS: module-qualified consent types
ConsentOutcome and ConsentDecisionType now live in the CMP module. import EzoicAdsSDK still makes them available under the same names, but a name qualified with the ads module, such as EzoicAdsSDK.ConsentOutcome, no longer compiles. Drop the module prefix and write ConsentOutcome.
CocoaPods
The CocoaPods trunk has no Ezoic SDK newer than 1.19.1, and it becomes read-only on December 2, 2026. From 1.22.0:
- Native iOS apps: install with Swift Package Manager. The native SDK isn't available as a pod.
- Flutter and Capacitor: CocoaPods builds work again and get native SDK 1.22.0. The plugins' pods now include the native frameworks themselves instead of depending on the trunk pod. Flutter downloads them from the SDK's GitHub release when you run
pod install; Capacitor ships them in the npm package. If yourPodfilelistspod 'EzoicAdsSDK'itself, remove it. - React Native: nothing changes. The npm package already includes the native frameworks, and now includes the CMP framework as well.
- Unity: iOS builds need the Swift package. The CocoaPods fallback was removed, so keep Swift Package Manager Integration on in the External Dependency Manager's iOS Resolver, or add the package in Xcode. See Unity: iOS.