To handle AdMob GDPR consent in a Capacitor app, use Google’s User Messaging Platform (UMP) SDK: request consent information on every launch, show the consent form if required, and only initialize ads when canRequestAds is true. You also need a privacy options entry point so users can change their choice, and on iOS the UMP flow should come before the App Tracking Transparency prompt. This guide shows the full flow with two setups: a plugin that wraps UMP in JavaScript, and @capgo/capacitor-admob with a small native consent bridge.
What UMP does and why AdMob requires it
Since January 2024, Google requires publishers that serve ads to users in the EEA, the UK and Switzerland to use a Google-certified consent management platform (CMP) integrated with the IAB Transparency and Consent Framework (TCF). The UMP SDK is Google’s own certified CMP, free and built into the AdMob console.
UMP does three things for you:
- Detects whether the user is in a region where consent is required.
- Shows the consent message you configured in AdMob, in the user’s language.
- Stores the user’s choices as a TC string on the device (the standard
IABTCF_keys inUserDefaultson iOS andSharedPreferenceson Android), which the Google Mobile Ads SDK and mediation partners read when requesting ads.
You can also use UMP for US state privacy regulations, which AdMob calls “US states” messages, from the same console.
Step 1: create the messages in AdMob
In the AdMob console, go to Privacy & messaging:
- Create a European regulations message (GDPR). Choose which apps it applies to, the languages, and your list of ad partners.
- Optionally create a US states message.
- On iOS, create an IDFA explainer message if you plan to request App Tracking Transparency. UMP will show it before the system ATT prompt.
- Publish the messages. An unpublished message will never show.
If the form never appears in your app later, an unpublished or unassigned message is the first thing to check.
Step 2: decide the order of operations
The order that works for most apps:
- App starts.
- Request consent info update with UMP (every launch).
- If required, load and show the consent form.
- If
canRequestAdsis true, start the Mobile Ads SDK and load ads. - Show a “Privacy settings” entry in your settings screen when UMP says privacy options are required.
Google also allows starting the Mobile Ads SDK in parallel with the consent request when canRequestAds was already true from a previous session. That shortens time to first ad for returning users.
Option A: a plugin that wraps UMP in JavaScript
@capacitor-community/admob (version 8 supports Capacitor 8) exposes UMP methods directly: requestConsentInfo, showConsentForm, showPrivacyOptionsForm and resetConsentInfo.
bun add @capacitor-community/admob
bunx cap sync
import {
AdMob,
AdmobConsentStatus,
AdmobConsentDebugGeography,
} from '@capacitor-community/admob';
export async function initAdsWithConsent(isDebug: boolean) {
await AdMob.initialize();
let info = await AdMob.requestConsentInfo(
isDebug
? {
debugGeography: AdmobConsentDebugGeography.EEA,
testDeviceIdentifiers: ['YOUR_TEST_DEVICE_ID'],
}
: undefined,
);
if (info.isConsentFormAvailable && info.status === AdmobConsentStatus.REQUIRED) {
info = await AdMob.showConsentForm();
}
// The enum value is the string 'REQUIRED'
const showPrivacyEntry = String(info.privacyOptionsRequirementStatus) === 'REQUIRED';
return { canRequestAds: info.canRequestAds, showPrivacyEntry };
}
export async function openPrivacyOptions() {
await AdMob.showPrivacyOptionsForm();
}
This plugin is the one exception to the order above: its README requires AdMob.initialize() before requestConsentInfo(), and on iOS the consent form cannot be presented until initialize() has run. initialize() starts the SDK but does not request ads, so the rule that matters is never to load an ad until canRequestAds is true. If you need the SDK to stay stopped until the user has answered, use Option B, where consent runs before AdMob.start().
Check canRequestAds before every ad load, not just at startup, because the user can withdraw consent from the privacy options form.
Option B: @capgo/capacitor-admob with a native UMP bridge
@capgo/capacitor-admob gives you AdMob.start(), request configuration, banner, interstitial, rewarded and rewarded interstitial ads, plus iOS tracking authorization. It does not wrap UMP, so you add a small app-local plugin that runs the consent flow before you call AdMob.start().
bun add @capgo/capacitor-admob
bunx cap sync
Add the UMP SDK
- iOS: add Google’s
GoogleUserMessagingPlatformpackage with Swift Package Manager (Capacitor 8 projects use SPM by default), or the CocoaPods pod of the same name if your project still uses CocoaPods. - Android: in
android/app/build.gradle:
dependencies {
implementation "com.google.android.ump:user-messaging-platform:<latest version>"
}
Use the latest release from Google’s Maven repository.
iOS consent plugin
Create ios/App/App/ConsentPlugin.swift:
import Capacitor
import UserMessagingPlatform
@objc(ConsentPlugin)
public class ConsentPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "ConsentPlugin"
public let jsName = "Consent"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "gather", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "showPrivacyOptions", returnType: CAPPluginReturnPromise),
]
@objc func gather(_ call: CAPPluginCall) {
let parameters = RequestParameters()
DispatchQueue.main.async {
ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { error in
if let error = error {
call.reject(error.localizedDescription)
return
}
guard let vc = self.bridge?.viewController else {
call.reject("No view controller")
return
}
ConsentForm.loadAndPresentIfRequired(from: vc) { formError in
if let formError = formError {
call.reject(formError.localizedDescription)
return
}
call.resolve(self.state())
}
}
}
}
@objc func showPrivacyOptions(_ call: CAPPluginCall) {
DispatchQueue.main.async {
guard let vc = self.bridge?.viewController else {
call.reject("No view controller")
return
}
ConsentForm.presentPrivacyOptionsForm(from: vc) { formError in
if let formError = formError {
call.reject(formError.localizedDescription)
return
}
call.resolve(self.state())
}
}
}
private func state() -> [String: Any] {
return [
"canRequestAds": ConsentInformation.shared.canRequestAds,
"privacyOptionsRequired":
ConsentInformation.shared.privacyOptionsRequirementStatus == .required,
]
}
}
Register it from a CAPBridgeViewController subclass, and set that class on the view controller in Main.storyboard:
import Capacitor
class MainViewController: CAPBridgeViewController {
override open func capacitorDidLoad() {
bridge?.registerPluginInstance(ConsentPlugin())
}
}
Android consent plugin
Create android/app/src/main/java/com/example/app/ConsentPlugin.java (use your package name):
package com.example.app;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.google.android.ump.ConsentInformation;
import com.google.android.ump.ConsentRequestParameters;
import com.google.android.ump.UserMessagingPlatform;
@CapacitorPlugin(name = "Consent")
public class ConsentPlugin extends Plugin {
@PluginMethod
public void gather(PluginCall call) {
ConsentInformation info = UserMessagingPlatform.getConsentInformation(getContext());
ConsentRequestParameters params = new ConsentRequestParameters.Builder().build();
getActivity().runOnUiThread(() ->
info.requestConsentInfoUpdate(
getActivity(),
params,
() -> UserMessagingPlatform.loadAndShowConsentFormIfRequired(getActivity(), formError -> {
if (formError != null) {
call.reject(formError.getMessage());
return;
}
call.resolve(state(info));
}),
requestError -> call.reject(requestError.getMessage())
)
);
}
@PluginMethod
public void showPrivacyOptions(PluginCall call) {
getActivity().runOnUiThread(() ->
UserMessagingPlatform.showPrivacyOptionsForm(getActivity(), formError -> {
if (formError != null) {
call.reject(formError.getMessage());
return;
}
call.resolve(state(UserMessagingPlatform.getConsentInformation(getContext())));
})
);
}
private JSObject state(ConsentInformation info) {
JSObject ret = new JSObject();
ret.put("canRequestAds", info.canRequestAds());
ret.put(
"privacyOptionsRequired",
info.getPrivacyOptionsRequirementStatus() ==
ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
);
return ret;
}
}
Register it in MainActivity.java before super.onCreate:
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(ConsentPlugin.class);
super.onCreate(savedInstanceState);
}
}
Call it from TypeScript
import { registerPlugin } from '@capacitor/core';
import { AdMob, BannerAd } from '@capgo/capacitor-admob';
interface ConsentState {
canRequestAds: boolean;
privacyOptionsRequired: boolean;
}
interface ConsentPlugin {
gather(): Promise<ConsentState>;
showPrivacyOptions(): Promise<ConsentState>;
}
const Consent = registerPlugin<ConsentPlugin>('Consent');
export async function startAds() {
let state: ConsentState;
try {
state = await Consent.gather();
} catch (err) {
console.warn('Consent flow failed', err);
return { showPrivacyEntry: false };
}
if (state.canRequestAds) {
await AdMob.start();
const banner = new BannerAd({
adUnitId: 'ca-app-pub-3940256099942544/2934735716', // Google test banner (iOS)
position: 'bottom',
});
await banner.show();
}
return { showPrivacyEntry: state.privacyOptionsRequired };
}
export const openPrivacyOptions = () => Consent.showPrivacyOptions();
Native code changes need a new store build. After that, consent copy in your settings screen and ad placement logic can be shipped with Capgo live updates.
Step 3: add the privacy options entry point
When UMP reports that privacy options are required, Google expects a visible way to reopen the consent choices. Add a “Privacy settings” row to your settings page and hide it when not required:
const { showPrivacyEntry } = await startAds();
document.querySelector('#privacy-row')?.toggleAttribute('hidden', !showPrivacyEntry);
document.querySelector('#privacy-row')?.addEventListener('click', () => openPrivacyOptions());
After the user changes their choices, read canRequestAds again before the next ad request.
Step 4: App Tracking Transparency on iOS
ATT is separate from GDPR. ATT controls access to the IDFA on iOS. GDPR consent controls whether and how personal data can be processed. You usually need both for an EU user on iOS.
- Add
NSUserTrackingUsageDescriptiontoInfo.plist. - Let UMP show the IDFA explainer message and trigger ATT, if you configured that message in AdMob. Otherwise, request ATT yourself after the consent flow.
With @capgo/capacitor-admob you can read and request the ATT status directly:
import { AdMob, TrackingAuthorizationStatus } from '@capgo/capacitor-admob';
const { status } = await AdMob.trackingAuthorizationStatus();
if (status === TrackingAuthorizationStatus.notDetermined) {
await AdMob.requestTrackingAuthorization();
}
Capgo also has a standalone App Tracking Transparency plugin if you need ATT for other SDKs. Do not forget to declare tracking in your privacy manifest and App Store privacy labels, see our privacy manifest guide.
Step 5: test with debug geography
UMP only shows the GDPR form to users it locates in the EEA, UK or Switzerland. To test elsewhere:
- Run the app once and find the log line from UMP with your device’s hashed ID.
- Add it as a test device and set debug geography to EEA. With the community plugin, pass
debugGeographyandtestDeviceIdentifierstorequestConsentInfo. In a native bridge, setDebugSettingson iOS andConsentDebugSettingson Android in debug builds only. - Reset consent between runs (
resetConsentInfo()in the community plugin,ConsentInformation.reset()natively) so the form shows again.
Never ship debug settings in release builds. Gate them on a build flag.
Step 6: handle errors without blocking the app
Consent requests can fail: no network, misconfigured app ID, or no published message. Treat failure as “no ads for now”, not as a crash:
- If
requestConsentInfoUpdatefails butcanRequestAdswas true from a previous session, you can still request ads. - If the form fails to load, log it and retry on the next launch.
- Never show ads while the consent form is on screen.
Troubleshooting
The form never shows. Message not published, not assigned to this app ID, or you are outside the EEA without debug geography. Check the AdMob app ID in Info.plist and AndroidManifest.xml matches the console.
The form shows every launch. The user closed it without a choice, or you call reset in production code.
canRequestAds false for US users. You published a US states message and the user opted out of sale or sharing. Serve limited ads only through the SDK, do not bypass it.
Mediation partners get no consent. Make sure each partner adapter is up to date and listed in your GDPR message’s ad partner list.
iOS ATT prompt never appears. NSUserTrackingUsageDescription is missing, or the user disabled “Allow Apps to Request to Track” in Settings.
Compliance beyond the form
UMP handles the ad side. Your app still needs a privacy policy that lists AdMob, data retention rules and a way to handle data requests. Our GDPR compliance checklist covers the rest of the app.