Ad Revenue Attribution
Track ad revenue from mediation platforms and attribute it to specific marketing campaigns that brought users to your app, enabling comprehensive ROI analysis.
Ad Revenue Attribution connects mobile app ad revenue to the marketing campaigns that generated users, combining campaign cost, in-app revenue, and ad revenue in unified reporting. This data also flows back to ad networks to optimize campaign performance.
Learn More: See Singular Ad Revenue Attribution FAQ for detailed information on attribution methodology and supported mediation platforms.
How Ad Revenue Attribution Works
Attribution Flow
Your mediation platform reports impression-level or user-level revenue data through callbacks, which you validate and forward to Singular for attribution analysis.
- Campaign Attribution: Ties ad revenue to the specific campaigns that acquired each user, showing true ROI per campaign
- Data Sources: Revenue data comes from your mediation platform at either user-level or impression-level granularity
- Network Optimization: Singular passes revenue data back to ad networks to improve targeting and bidding strategies
Implementation Requirements
Ensure data accuracy before implementing ad revenue tracking, as incorrect revenue data cannot be corrected after transmission.
Critical Requirements:
- Currency Codes: Use three-letter ISO 4217 codes (USD, EUR, INR). Most mediation platforms use USD by default
- Data Validation: Always validate revenue values are positive and currency codes are non-empty before sending to Singular
- Unit Conversion: Some platforms report revenue in micros (1,000,000 = $1.00). Convert to dollars before sending to Singular
Setup Steps
Follow these steps to implement ad revenue attribution with your mediation platform.
- Update SDK: Ensure you're running the latest version of the Singular Flutter SDK
- Configure Platform: Enable ad revenue reporting in your mediation platform dashboard (AdMob, AppLovin, etc.)
- Implement Callbacks: Add revenue event listeners from your mediation SDK to capture impression data
- Validate Data: Check revenue > 0 and currency is valid before forwarding to Singular
- Test Integration: Verify revenue data appears in Singular reporting within 24 hours
AdMob Integration
Prerequisites
Enable ad revenue reporting in your AdMob account and implement the Google Mobile Ads SDK for Flutter before integrating with Singular.
- AdMob Setup: Enable impression-level ad revenue in your AdMob dashboard. See AdMob Support
- Flutter Package: Install google_mobile_ads package. See Getting Started Guide
Platform Differences: AdMob reports revenue differently by platform. Android returns revenue in micros (5000 = $0.005), while iOS returns decimal values (0.005 = $0.005). Convert Android values by dividing by 1,000,000 before sending to Singular.
Implementation Steps
Set up the onPaidEvent callback when loading ads to capture revenue data from successful ad impressions.
- Load Ad: Create and load an ad unit (Rewarded, Interstitial, Banner, etc.)
-
Set Callback: Assign
onPaidEventcallback to captureAdValuewhen ad generates revenue -
Convert Units: Divide
valueMicrosby 1,000,000 to convert to dollars - Validate: Check revenue > 0 and currency is non-empty
-
Send to Singular: Call
Singular.adRevenue()with validated data
Rewarded Ad Example
Capture ad revenue from rewarded video ads by setting the
onPaidEvent callback after ad load completes.
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';
const String adUnitId = 'YOUR_AD_UNIT_ID';
class AdManager {
RewardedAd? _rewardedAd;
void loadRewardedAd() {
RewardedAd.load(
adUnitId: adUnitId,
request: AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (RewardedAd ad) {
_rewardedAd = ad;
print('Rewarded ad loaded successfully');
// Set up full screen content callback
_rewardedAd?.fullScreenContentCallback = FullScreenContentCallback(
onAdShowedFullScreenContent: () {
print('Rewarded ad displayed');
},
onAdFailedToShowFullScreenContent: (AdError adError) {
print('Rewarded ad failed to show: ${adError.message}');
},
onAdDismissedFullScreenContent: () {
print('Rewarded ad dismissed');
_rewardedAd = null;
},
);
// Set up paid event callback for revenue tracking
_rewardedAd?.onPaidEvent = (AdValue adValue) {
// Convert revenue from micros to dollars
double revenue = adValue.valueMicros / 1_000_000.0;
String? currency = adValue.currencyCode;
// Validate revenue and currency before sending
if (revenue > 0 && currency != null && currency.isNotEmpty) {
final adData = {
'adPlatform': 'AdMob',
'currency': currency,
'revenue': revenue,
};
// Send ad revenue data to Singular
Singular.adRevenue(adData);
// Log for debugging
print('Ad Revenue reported: $revenue $currency');
} else {
print('Invalid ad revenue: revenue=$revenue, currency=$currency');
}
};
},
onAdFailedToLoad: (LoadAdError loadAdError) {
print('Rewarded ad failed to load: ${loadAdError.message}');
},
),
);
}
void showRewardedAd() {
if (_rewardedAd != null) {
_rewardedAd!.show(
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
print('User earned reward: ${reward.amount} ${reward.type}');
},
);
} else {
print('Rewarded ad not ready');
}
}
}
Interstitial Ad Example
Track revenue from interstitial ads using the same
onPaidEvent pattern.
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';
const String interstitialAdUnitId = 'YOUR_INTERSTITIAL_AD_UNIT_ID';
class InterstitialAdManager {
InterstitialAd? _interstitialAd;
void loadInterstitialAd() {
InterstitialAd.load(
adUnitId: interstitialAdUnitId,
request: AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (InterstitialAd ad) {
_interstitialAd = ad;
print('Interstitial ad loaded');
// Set paid event callback
_interstitialAd?.onPaidEvent = (AdValue adValue) {
double revenue = adValue.valueMicros / 1_000_000.0;
String? currency = adValue.currencyCode;
if (revenue > 0 && currency != null && currency.isNotEmpty) {
Singular.adRevenue({
'adPlatform': 'AdMob',
'currency': currency,
'revenue': revenue,
});
print('Interstitial revenue: $revenue $currency');
}
};
// Set full screen callback
_interstitialAd?.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: () {
_interstitialAd = null;
loadInterstitialAd(); // Load next ad
},
);
},
onAdFailedToLoad: (LoadAdError error) {
print('Interstitial ad failed to load: ${error.message}');
},
),
);
}
void showInterstitialAd() {
_interstitialAd?.show();
}
}
Banner Ad Example
Track revenue from banner ads displayed in your Flutter UI.
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:singular_flutter_sdk/singular.dart';
const String bannerAdUnitId = 'YOUR_BANNER_AD_UNIT_ID';
class BannerAdWidget extends StatefulWidget {
@override
_BannerAdWidgetState createState() => _BannerAdWidgetState();
}
class _BannerAdWidgetState extends State<BannerAdWidget> {
BannerAd? _bannerAd;
bool _isBannerAdReady = false;
@override
void initState() {
super.initState();
_loadBannerAd();
}
void _loadBannerAd() {
_bannerAd = BannerAd(
adUnitId: bannerAdUnitId,
size: AdSize.banner,
request: AdRequest(),
listener: BannerAdListener(
onAdLoaded: (Ad ad) {
setState(() {
_isBannerAdReady = true;
});
print('Banner ad loaded');
// Set paid event callback
(ad as BannerAd).onPaidEvent = (AdValue adValue) {
double revenue = adValue.valueMicros / 1_000_000.0;
String? currency = adValue.currencyCode;
if (revenue > 0 && currency != null && currency.isNotEmpty) {
Singular.adRevenue({
'adPlatform': 'AdMob',
'currency': currency,
'revenue': revenue,
});
print('Banner revenue: $revenue $currency');
}
};
},
onAdFailedToLoad: (Ad ad, LoadAdError error) {
ad.dispose();
print('Banner ad failed to load: ${error.message}');
},
),
);
_bannerAd?.load();
}
@override
void dispose() {
_bannerAd?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isBannerAdReady && _bannerAd != null) {
return Container(
alignment: Alignment.center,
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
child: AdWidget(ad: _bannerAd!),
);
}
return SizedBox.shrink();
}
}
AppLovin MAX Integration
Prerequisites
Implement the AppLovin MAX Flutter plugin and configure impression-level revenue tracking before integrating with Singular.
- MAX Setup: Configure your app in the AppLovin MAX dashboard
- Flutter Plugin: Install applovin_max package. See Getting Started Guide
- Revenue API: Enable Impression-Level User Revenue API in MAX dashboard settings
Implementation Overview
Set up onAdRevenuePaidCallback listeners for each ad format to capture revenue when impressions generate earnings.
- Initialize MAX: Configure AppLovin MAX SDK with your SDK key
-
Set Listeners: Add
onAdRevenuePaidCallbackfor each ad format -
Extract Revenue: Get revenue from
ad.revenueproperty -
Currency Handling: AppLovin typically reports in USD, but verify with
ad.currency -
Forward to Singular: Send validated data to
Singular.adRevenue()
Complete MAX Implementation
Capture revenue from all MAX ad formats (Rewarded, Interstitial, Banner, MREC) using a unified handler.
import 'package:flutter/material.dart';
import 'package:applovin_max/applovin_max.dart';
import 'package:singular_flutter_sdk/singular.dart';
class AppLovinMaxManager extends StatefulWidget {
@override
_AppLovinMaxManagerState createState() => _AppLovinMaxManagerState();
}
class _AppLovinMaxManagerState extends State<AppLovinMaxManager> {
@override
void initState() {
super.initState();
_initializeAppLovinMax();
}
Future<void> _initializeAppLovinMax() async {
// Initialize AppLovin MAX SDK
await AppLovinMAX.initialize('YOUR_SDK_KEY');
// Set up revenue listeners for all ad formats
_setupRewardedAdListeners();
_setupInterstitialAdListeners();
_setupBannerAdListeners();
_setupMRecAdListeners();
print('AppLovin MAX initialized with revenue tracking');
}
void _setupRewardedAdListeners() {
AppLovinMAX.setRewardedAdListener(RewardedAdListener(
onAdRevenuePaidCallback: (ad) {
_handleAdRevenuePaid(ad, 'Rewarded');
},
onAdLoadedCallback: (ad) {
print('Rewarded ad loaded');
},
onAdLoadFailedCallback: (adUnitId, error) {
print('Rewarded ad failed to load: $error');
},
));
}
void _setupInterstitialAdListeners() {
AppLovinMAX.setInterstitialListener(InterstitialListener(
onAdRevenuePaidCallback: (ad) {
_handleAdRevenuePaid(ad, 'Interstitial');
},
onAdLoadedCallback: (ad) {
print('Interstitial ad loaded');
},
onAdLoadFailedCallback: (adUnitId, error) {
print('Interstitial ad failed to load: $error');
},
));
}
void _setupBannerAdListeners() {
AppLovinMAX.setBannerListener(AdViewAdListener(
onAdRevenuePaidCallback: (ad) {
_handleAdRevenuePaid(ad, 'Banner');
},
onAdLoadedCallback: (ad) {
print('Banner ad loaded');
},
onAdLoadFailedCallback: (adUnitId, error) {
print('Banner ad failed to load: $error');
},
));
}
void _setupMRecAdListeners() {
AppLovinMAX.setMRecListener(AdViewAdListener(
onAdRevenuePaidCallback: (ad) {
_handleAdRevenuePaid(ad, 'MREC');
},
onAdLoadedCallback: (ad) {
print('MREC ad loaded');
},
onAdLoadFailedCallback: (adUnitId, error) {
print('MREC ad failed to load: $error');
},
));
}
void _handleAdRevenuePaid(Ad ad, String adType) {
// Extract revenue and currency from ad object
final double revenueValue = ad.revenue ?? 0.0;
final String currency = ad.revenueCurrency ?? 'USD';
// Validate revenue before sending
if (revenueValue > 0) {
final adData = {
'adPlatform': 'AppLovin',
'currency': currency,
'revenue': revenueValue,
};
// Send ad revenue to Singular
Singular.adRevenue(adData);
// Log for debugging
print('[$adType] Revenue: $revenueValue $currency');
} else {
print('[$adType] Invalid revenue: $revenueValue');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('AppLovin MAX Revenue Tracking'),
),
body: Center(
child: Text('MAX revenue tracking active'),
),
);
}
}
Best Practice: Set up ad revenue listeners during app initialization before loading any ads to ensure no impressions are missed.
IronSource (Unity LevelPlay) Integration
Prerequisites
Enable Impression Level Revenue (ILR) in your IronSource dashboard and implement the IronSource Flutter plugin before integrating with Singular.
- IronSource Setup: Enable ARM SDK Postbacks Flag in IronSource dashboard
- Flutter Plugin: Install ironsource_mediation package. See Getting Started Guide
- ILR API: Configure impression-level revenue tracking for your app
Implementation Example
Use the onImpressionDataSuccess callback to capture impression-level
revenue data from IronSource mediation.
import 'package:ironsource_mediation/ironsource_mediation.dart';
import 'package:singular_flutter_sdk/singular.dart';
void setupIronSourceRevenueTracking() {
// Set up impression data listener
IronSource.setImpressionDataListener((ISImpressionData? impressionData) {
onImpressionDataSuccess(impressionData);
});
}
void onImpressionDataSuccess(ISImpressionData? impressionData) {
// Ensure impression data is not null
if (impressionData == null) {
print('No impression data available');
return;
}
// Extract and validate revenue
final revenue = impressionData.revenue?.toDouble() ?? 0.0;
if (revenue <= 0) {
print('Invalid revenue value: $revenue');
return;
}
// Create ad revenue data for Singular
final adData = {
'adPlatform': 'IronSource',
'currency': 'USD',
'revenue': revenue,
};
// Send to Singular
Singular.adRevenue(adData);
// Log for debugging
print('IronSource Revenue: $revenue USD');
}
TradPlus Integration
Prerequisites
Set up the TradPlus impression delegate to capture eCPM data when ads generate revenue.
- TradPlus Setup: Configure your app in the TradPlus dashboard
- Impression Delegate: Enable impression-level tracking in TradPlus settings
eCPM Conversion: TradPlus typically reports eCPM in milli-units. Divide by 1000.0 to convert to dollars before sending to Singular.
Implementation Example
Use the global impression listener to capture revenue data from all TradPlus ad formats.
import 'package:singular_flutter_sdk/singular.dart';
void setupTradPlusImpressionListener() {
// Set up global impression listener
TradPlusSdk.setGlobalImpressionListener((tpAdInfo) {
if (tpAdInfo == null) {
print('AdInfo is null');
return;
}
// Ensure eCPM is available
if (tpAdInfo.ecpm == null) {
print('eCPM value is null');
return;
}
// Convert eCPM from milli-units to dollars
double revenue = tpAdInfo.ecpm! / 1000.0;
// Validate revenue
if (revenue <= 0) {
print('Invalid revenue: $revenue');
return;
}
// Create ad revenue data
final adData = {
'adPlatform': 'TradPlus',
'currency': 'USD',
'revenue': revenue,
};
// Send to Singular
Singular.adRevenue(adData);
// Log for debugging
print('TradPlus Revenue: $revenue USD');
});
}
void main() {
// Initialize TradPlus SDK
setupTradPlusImpressionListener();
runApp(MyApp());
}
Generic Integration (Other Platforms)
Custom Implementation
For mediation platforms not explicitly covered above, implement a custom revenue handler using the Singular ad revenue method.
import 'package:singular_flutter_sdk/singular.dart';
/// Generic function to report ad revenue to Singular
/// Use this for any mediation platform not explicitly supported above
void reportAdRevenue({
required String adPlatform,
required String currency,
required double revenue,
}) {
// Validate revenue value
if (revenue <= 0) {
print('Invalid revenue value: $revenue');
return;
}
// Validate currency code
if (currency.isEmpty) {
print('Currency code is empty');
return;
}
// Create ad revenue data
final adData = {
'adPlatform': adPlatform,
'currency': currency,
'revenue': revenue,
};
// Send to Singular
Singular.adRevenue(adData);
// Log for debugging
print('Ad Revenue reported: $revenue $currency from $adPlatform');
}
// Example usage with a custom mediation platform
void onCustomAdImpression(Map<String, dynamic> impressionData) {
reportAdRevenue(
adPlatform: 'CustomNetwork',
currency: impressionData['currency'] ?? 'USD',
revenue: impressionData['revenue'] ?? 0.0,
);
}
Best Practices and Troubleshooting
Data Validation
- Always Validate: Check revenue > 0 and currency is non-empty before sending to Singular
- Unit Conversion: Verify whether your platform reports in micros or dollars and convert accordingly
- Currency Codes: Use ISO 4217 three-letter codes (USD, EUR, JPY) consistently
- Test Thoroughly: Verify revenue data appears in Singular reporting before production release
Common Issues
- Missing Revenue: Ensure impression-level revenue is enabled in your mediation platform dashboard
- Incorrect Values: Check unit conversion (micros vs dollars) for your specific platform
- No Data in Reports: Allow 24-48 hours for data to appear in Singular reporting after implementation
- Null Revenue: Verify ad callbacks are properly set before ad load completes
Important: Ad revenue data cannot be corrected after transmission. Always validate data accuracy before calling Singular.adRevenue().