Ad Revenue Attribution
Connect ad revenue to specific marketing campaigns that brought users to your app, providing complete visibility into campaign costs, in-app revenue, and ad revenue for accurate ROI measurement.
Overview
What is Ad Revenue Attribution
Ad Revenue Attribution ties mobile app ad revenue to the marketing campaigns that generated users, enabling you to measure true campaign performance by connecting user acquisition costs with lifetime revenue including ad monetization.
- Campaign ROI: View campaign cost, in-app purchases, and ad revenue in unified reports to calculate true return on ad spend
- Network Optimization: Send ad revenue data back to ad networks to improve bidding algorithms and campaign performance
- Data Sources: Supports user-level and impression-level data from mediation platforms including AdMob, AppLovin MAX, Unity LevelPlay (IronSource), and TradPlus
For complete details, see the Ad Revenue Attribution FAQ.
Important Considerations:
- Currency Codes: Use ISO 4217 three-letter currency codes (USD, EUR, INR). Most mediation platforms report in USD—verify your platform's currency before implementation
- Data Accuracy: Validate revenue and currency data before sending to Singular. Incorrect data cannot be corrected retroactively
Implementation Requirements
Ad revenue tracking requires integration with your mediation platform SDK and configuration of revenue callbacks.
- SDK Version: Update to the latest Singular React Native SDK version
- Mediation Platform: Integrate the React Native SDK for your mediation platform (AdMob, AppLovin MAX, IronSource, or TradPlus)
- Revenue Callbacks: Implement platform-specific paid event handlers to capture impression-level revenue data
- Validation Logic: Add revenue and currency validation before sending data to Singular
SDK Method
Singular.adRevenue
Report ad revenue data to Singular with platform, currency, and revenue amount for attribution to the user's acquisition campaign.
Method Signature:
static adRevenue(data: {
adPlatform: string;
currency: string;
revenue: number;
}): void
Parameters:
- adPlatform: Mediation platform name (e.g., "AdMob", "AppLovin", "IronSource", "TradPlus")
- currency: ISO 4217 three-letter currency code (e.g., "USD", "EUR")
- revenue: Revenue amount in the specified currency (must be greater than 0)
For complete method documentation, see adRevenue reference.
Platform Integrations
AdMob Integration
Implement AdMob ad revenue tracking using the Google Mobile Ads SDK paid event callbacks for impression-level revenue reporting.
Prerequisites
- Enable ad revenue reporting in your AdMob account. See AdMob Support
- Integrate the React Native Google Mobile Ads package. See Getting Started Guide
Implementation Overview
When loading ad formats (App Open, Banner, Interstitial, Native, Rewarded), configure a paid event handler that triggers when ads generate revenue. Extract the revenue value and currency from the event data, validate both values, and send to Singular.
Platform Difference: AdMob reports revenue differently by platform. Android reports revenue in micros (e.g., $0.005 appears as 5000), requiring division by 1,000,000. iOS reports revenue in dollars directly (0.005). Adjust conversion logic based on platform detection.
AdMob Rewarded Ad Example
Load a rewarded ad with AdMob and capture paid events for revenue tracking.
// TurboModule direct API (React Native 0.76+ New Architecture)
import React, { useEffect } from 'react';
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { RewardedAd, AdEventType } from 'react-native-google-mobile-ads';
import { Platform } from 'react-native';
const AD_UNIT_ID = 'ca-app-pub-xxxxxxxxxxxxx/yyyyyyyyyy';
export default function AdMobRevenueTracker() {
useEffect(() => {
loadRewardedAd();
}, []);
const loadRewardedAd = () => {
// Create RewardedAd instance
const rewardedAd = RewardedAd.createForAdRequest(AD_UNIT_ID);
// Set up event listener for ad events
rewardedAd.addAdEventListener((type, error, data) => {
if (type === AdEventType.LOADED) {
console.log('Rewarded ad loaded');
} else if (type === AdEventType.ERROR) {
console.error('Rewarded ad failed to load:', error);
} else if (type === AdEventType.PAID_EVENT) {
// Handle paid event with revenue data
handleAdRevenue(data);
}
});
// Load the ad
rewardedAd.load();
};
const handleAdRevenue = (data) => {
const { value, currencyCode } = data;
// Validate revenue and currency
if (!value || value <= 0) {
console.error('Invalid ad revenue value:', value);
return;
}
if (!currencyCode || currencyCode.trim() === '') {
console.error('Invalid currency code:', currencyCode);
return;
}
// Convert revenue based on platform
let revenue;
if (Platform.OS === 'android') {
// Android reports in micros - convert to dollars
revenue = value / 1_000_000.0;
} else {
// iOS reports in dollars directly
revenue = value;
}
const adRevenueData = {
adPlatform: 'AdMob',
currency: currencyCode,
revenue: revenue
};
// Send to Singular
NativeSingular.adRevenue(adRevenueData);
console.log('Ad Revenue reported to Singular:', adRevenueData);
};
return null;
}
import React, { useEffect } from 'react';
import { Singular } from 'singular-react-native';
import { RewardedAd, AdEventType } from 'react-native-google-mobile-ads';
import { Platform } from 'react-native';
const AD_UNIT_ID = 'ca-app-pub-xxxxxxxxxxxxx/yyyyyyyyyy';
export default function AdMobRevenueTracker() {
useEffect(() => {
loadRewardedAd();
}, []);
const loadRewardedAd = () => {
// Create RewardedAd instance
const rewardedAd = RewardedAd.createForAdRequest(AD_UNIT_ID);
// Set up event listener for ad events
rewardedAd.addAdEventListener((type, error, data) => {
if (type === AdEventType.LOADED) {
console.log('Rewarded ad loaded');
} else if (type === AdEventType.ERROR) {
console.error('Rewarded ad failed to load:', error);
} else if (type === AdEventType.PAID_EVENT) {
// Handle paid event with revenue data
handleAdRevenue(data);
}
});
// Load the ad
rewardedAd.load();
};
const handleAdRevenue = (data) => {
const { value, currencyCode } = data;
// Validate revenue and currency
if (!value || value <= 0) {
console.error('Invalid ad revenue value:', value);
return;
}
if (!currencyCode || currencyCode.trim() === '') {
console.error('Invalid currency code:', currencyCode);
return;
}
// Convert revenue based on platform
let revenue;
if (Platform.OS === 'android') {
// Android reports in micros - convert to dollars
revenue = value / 1_000_000.0;
} else {
// iOS reports in dollars directly
revenue = value;
}
const adRevenueData = {
adPlatform: 'AdMob',
currency: currencyCode,
revenue: revenue
};
// Send to Singular
Singular.adRevenue(adRevenueData);
console.log('Ad Revenue reported to Singular:', adRevenueData);
};
return null;
}
Implementation Notes:
-
Platform Detection: Use
Platform.OSto determine conversion logic (Android divides by 1,000,000, iOS uses value directly) - Revenue Validation: Ensure revenue is greater than 0 before sending
- Currency Validation: Verify currency code is non-empty
- Error Logging: Log invalid data for debugging without sending to Singular
AppLovin MAX Integration
Implement AppLovin MAX ad revenue tracking using the Impression-Level User Revenue API for real-time revenue reporting across all ad formats.
Prerequisites
- Integrate the AppLovin MAX React Native SDK. See Getting Started Guide
- Enable Impression-Level User Revenue API in your AppLovin dashboard
Implementation Overview
Configure ad revenue listeners for each ad format (Interstitial, Rewarded,
Banner, MRec, App Open) to capture revenue events. Extract revenue from
adInfo.revenue and send to Singular with platform-specific
currency (typically USD).
AppLovin MAX Revenue Tracking
Set up global revenue listeners for all AppLovin MAX ad formats.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
import {
InterstitialAd,
RewardedAd,
BannerAd,
MRecAd,
AppOpenAd
} from 'react-native-applovin-max';
// Currency constant - AppLovin typically reports in USD
const CURRENCY = 'USD';
// Generic handler for ad revenue
const handleAdRevenue = (adInfo) => {
if (!adInfo) {
console.error('AdInfo is null or undefined');
return;
}
const revenue = adInfo.revenue;
// Validate revenue
if (!revenue || revenue <= 0) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: 'AppLovin',
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
NativeSingular.adRevenue(adRevenueData);
console.log('AppLovin ad revenue reported:', adRevenueData);
};
// Set up listeners for each ad type
export const setupAppLovinRevenueTracking = () => {
InterstitialAd.addAdRevenuePaidListener(handleAdRevenue);
RewardedAd.addAdRevenuePaidListener(handleAdRevenue);
BannerAd.addAdRevenuePaidListener(handleAdRevenue);
MRecAd.addAdRevenuePaidListener(handleAdRevenue);
AppOpenAd.addAdRevenuePaidListener(handleAdRevenue);
console.log('AppLovin MAX revenue tracking initialized');
};
import { Singular } from 'singular-react-native';
import {
InterstitialAd,
RewardedAd,
BannerAd,
MRecAd,
AppOpenAd
} from 'react-native-applovin-max';
// Currency constant - AppLovin typically reports in USD
const CURRENCY = 'USD';
// Generic handler for ad revenue
const handleAdRevenue = (adInfo) => {
if (!adInfo) {
console.error('AdInfo is null or undefined');
return;
}
const revenue = adInfo.revenue;
// Validate revenue
if (!revenue || revenue <= 0) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: 'AppLovin',
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
Singular.adRevenue(adRevenueData);
console.log('AppLovin ad revenue reported:', adRevenueData);
};
// Set up listeners for each ad type
export const setupAppLovinRevenueTracking = () => {
InterstitialAd.addAdRevenuePaidListener(handleAdRevenue);
RewardedAd.addAdRevenuePaidListener(handleAdRevenue);
BannerAd.addAdRevenuePaidListener(handleAdRevenue);
MRecAd.addAdRevenuePaidListener(handleAdRevenue);
AppOpenAd.addAdRevenuePaidListener(handleAdRevenue);
console.log('AppLovin MAX revenue tracking initialized');
};
Implementation Notes:
- All Ad Formats: Register listeners for all ad types your app uses (Interstitial, Rewarded, Banner, MRec, App Open)
- Currency: AppLovin typically reports revenue in USD—verify in your dashboard
- Shared Handler: Use a single revenue handler function for all ad formats to ensure consistent validation
Unity LevelPlay (IronSource) Integration
Implement IronSource ad revenue tracking using the Impression Level Revenue (ILR) SDK API for impression-level data from IronSource Ads and mediated networks.
Prerequisites
- Integrate the IronSource React Native SDK. See Getting Started Guide
- Enable ARM SDK Postbacks Flag in your IronSource dashboard
- Review IronSource Ad Revenue Measurement documentation
Implementation Overview
Subscribe to the onImpressionDataSuccess event emitter to
receive impression data. Extract revenue from
impressionData.revenue and send to Singular with USD currency.
IronSource Revenue Tracking
Configure event listener for IronSource impression data callbacks.
// TurboModule direct API (React Native 0.76+ New Architecture)
import { NativeModules, NativeEventEmitter } from 'react-native';
import NativeSingular from 'singular-react-native/js/NativeSingular';
const { IronSourceModule } = NativeModules;
const ironSourceEventEmitter = new NativeEventEmitter(IronSourceModule);
const AD_PLATFORM = 'IronSource';
const CURRENCY = 'USD'; // IronSource typically reports in USD
export const setupIronSourceRevenueTracking = () => {
ironSourceEventEmitter.addListener('onImpressionDataSuccess', (impressionData) => {
// Validate impression data
if (!impressionData) {
console.error('No impression data available');
return;
}
const revenue = impressionData.revenue;
// Validate revenue value
if (!revenue || revenue <= 0) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: AD_PLATFORM,
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
NativeSingular.adRevenue(adRevenueData);
console.log('IronSource ad revenue reported:', adRevenueData);
});
console.log('IronSource revenue tracking initialized');
};
import { NativeModules, NativeEventEmitter } from 'react-native';
import { Singular } from 'singular-react-native';
const { IronSourceModule } = NativeModules;
const ironSourceEventEmitter = new NativeEventEmitter(IronSourceModule);
const AD_PLATFORM = 'IronSource';
const CURRENCY = 'USD'; // IronSource typically reports in USD
export const setupIronSourceRevenueTracking = () => {
ironSourceEventEmitter.addListener('onImpressionDataSuccess', (impressionData) => {
// Validate impression data
if (!impressionData) {
console.error('No impression data available');
return;
}
const revenue = impressionData.revenue;
// Validate revenue value
if (!revenue || revenue <= 0) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: AD_PLATFORM,
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
Singular.adRevenue(adRevenueData);
console.log('IronSource ad revenue reported:', adRevenueData);
});
console.log('IronSource revenue tracking initialized');
};
Implementation Notes:
- Native Event Emitter: IronSource uses React Native's native event system for impression callbacks
- ARM Postbacks: Verify ARM SDK Postbacks Flag is enabled in IronSource dashboard
- Event Subscription: Set up listener before initializing IronSource SDK
TradPlus Integration
Implement TradPlus ad revenue tracking using global impression listeners to capture eCPM data from ad impressions.
Prerequisites
- Integrate the TradPlus React Native SDK with your app
- Configure TradPlus ad units in your dashboard
Implementation Overview
Subscribe to the onImpressionSuccess event to receive ad
impression data. Extract eCPM value from tpAdInfo.ecpm,
convert from millis to dollars (divide by 1000), and send to Singular.
eCPM Conversion: TradPlus reports eCPM in milli-units. Divide the eCPM value by 1000 to convert to dollar amounts before sending to Singular.
TradPlus Revenue Tracking
Configure event listener for TradPlus impression success callbacks.
// TurboModule direct API (React Native 0.76+ New Architecture)
import { NativeModules, NativeEventEmitter } from 'react-native';
import NativeSingular from 'singular-react-native/js/NativeSingular';
const { TradPlusModule } = NativeModules;
const tradPlusEventEmitter = new NativeEventEmitter(TradPlusModule);
const AD_PLATFORM = 'TradPlus';
const CURRENCY = 'USD'; // TradPlus typically reports in USD
export const setupTradPlusRevenueTracking = () => {
tradPlusEventEmitter.addListener('onImpressionSuccess', (tpAdInfo) => {
// Validate ad info
if (!tpAdInfo) {
console.error('AdInfo is null');
return;
}
// eCPM is reported in milli-units - convert to dollars
if (!tpAdInfo.ecpm || typeof tpAdInfo.ecpm !== 'number') {
console.error('Invalid eCPM value:', tpAdInfo.ecpm);
return;
}
const revenue = tpAdInfo.ecpm / 1000.0;
// Validate revenue after conversion
if (revenue <= 0) {
console.error('Revenue out of expected range:', revenue);
return;
}
const adRevenueData = {
adPlatform: AD_PLATFORM,
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
NativeSingular.adRevenue(adRevenueData);
console.log('TradPlus ad revenue reported:', adRevenueData);
});
console.log('TradPlus revenue tracking initialized');
};
import { NativeModules, NativeEventEmitter } from 'react-native';
import { Singular } from 'singular-react-native';
const { TradPlusModule } = NativeModules;
const tradPlusEventEmitter = new NativeEventEmitter(TradPlusModule);
const AD_PLATFORM = 'TradPlus';
const CURRENCY = 'USD'; // TradPlus typically reports in USD
export const setupTradPlusRevenueTracking = () => {
tradPlusEventEmitter.addListener('onImpressionSuccess', (tpAdInfo) => {
// Validate ad info
if (!tpAdInfo) {
console.error('AdInfo is null');
return;
}
// eCPM is reported in milli-units - convert to dollars
if (!tpAdInfo.ecpm || typeof tpAdInfo.ecpm !== 'number') {
console.error('Invalid eCPM value:', tpAdInfo.ecpm);
return;
}
const revenue = tpAdInfo.ecpm / 1000.0;
// Validate revenue after conversion
if (revenue <= 0) {
console.error('Revenue out of expected range:', revenue);
return;
}
const adRevenueData = {
adPlatform: AD_PLATFORM,
currency: CURRENCY,
revenue: revenue
};
// Send to Singular
Singular.adRevenue(adRevenueData);
console.log('TradPlus ad revenue reported:', adRevenueData);
});
console.log('TradPlus revenue tracking initialized');
};
Implementation Notes:
- eCPM Format: TradPlus reports eCPM in milli-units—always divide by 1000 before sending to Singular
- Type Checking: Verify eCPM is a number before conversion
- Post-Conversion Validation: Validate revenue is greater than 0 after division
Generic Integration
Implement ad revenue tracking for custom mediation platforms or direct
integrations using the generic adRevenue() method.
When to Use Generic Integration
- Custom mediation platforms not covered by standard integrations
- Direct ad network integrations without mediation
- Server-side ad revenue calculations forwarded to the app
- Testing ad revenue tracking with mock data
Generic Implementation Example
Create a reusable function to report ad revenue from any source with proper validation.
// TurboModule direct API (React Native 0.76+ New Architecture)
import NativeSingular from 'singular-react-native/js/NativeSingular';
/**
* Report ad revenue to Singular with validation
*
* @param {string} adPlatform - Name of the ad platform or mediation provider
* @param {string} currency - ISO 4217 three-letter currency code (e.g., 'USD', 'EUR')
* @param {number} revenue - Revenue amount in the specified currency
*/
export const reportAdRevenue = (adPlatform, currency, revenue) => {
// Validate platform
if (!adPlatform || adPlatform.trim() === '') {
console.error('Invalid ad platform:', adPlatform);
return;
}
// Validate currency code
if (!currency || currency.trim() === '' || currency.length !== 3) {
console.error('Invalid currency code:', currency);
return;
}
// Validate revenue
if (typeof revenue !== 'number' || revenue <= 0 || !isFinite(revenue)) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: adPlatform.trim(),
currency: currency.toUpperCase().trim(),
revenue: revenue
};
// Send to Singular
NativeSingular.adRevenue(adRevenueData);
console.log('Ad Revenue reported to Singular:', adRevenueData);
};
// Example usage
export const trackCustomAdRevenue = () => {
// Example: Custom mediation platform
reportAdRevenue('CustomPlatform', 'USD', 0.05);
// Example: Direct network integration
reportAdRevenue('FacebookAudienceNetwork', 'EUR', 0.03);
};
import { Singular } from 'singular-react-native';
/**
* Report ad revenue to Singular with validation
*
* @param {string} adPlatform - Name of the ad platform or mediation provider
* @param {string} currency - ISO 4217 three-letter currency code (e.g., 'USD', 'EUR')
* @param {number} revenue - Revenue amount in the specified currency
*/
export const reportAdRevenue = (adPlatform, currency, revenue) => {
// Validate platform
if (!adPlatform || adPlatform.trim() === '') {
console.error('Invalid ad platform:', adPlatform);
return;
}
// Validate currency code
if (!currency || currency.trim() === '' || currency.length !== 3) {
console.error('Invalid currency code:', currency);
return;
}
// Validate revenue
if (typeof revenue !== 'number' || revenue <= 0 || !isFinite(revenue)) {
console.error('Invalid revenue value:', revenue);
return;
}
const adRevenueData = {
adPlatform: adPlatform.trim(),
currency: currency.toUpperCase().trim(),
revenue: revenue
};
// Send to Singular
Singular.adRevenue(adRevenueData);
console.log('Ad Revenue reported to Singular:', adRevenueData);
};
// Example usage
export const trackCustomAdRevenue = () => {
// Example: Custom mediation platform
reportAdRevenue('CustomPlatform', 'USD', 0.05);
// Example: Direct network integration
reportAdRevenue('FacebookAudienceNetwork', 'EUR', 0.03);
};
Validation Features:
- Platform Validation: Ensures platform name is non-empty and trimmed
- Currency Validation: Verifies three-letter ISO 4217 code format and converts to uppercase
- Revenue Validation: Checks for positive number, excludes NaN and Infinity
- Type Safety: TypeScript interface ensures compile-time type checking
Best Practices
Data Validation
Implement robust validation to prevent incorrect data from reaching Singular analytics.
- Positive Revenue: Always verify revenue is greater than zero before sending
- Valid Currency: Use ISO 4217 codes and verify non-empty strings
- Platform Consistency: Use consistent platform names across your app (e.g., always "AdMob", not "Admob" or "ADMOB")
- Type Checking: Verify data types match expected values (number for revenue, string for currency/platform)
- Null Checks: Handle null, undefined, and empty values appropriately
Critical: Incorrect ad revenue data cannot be corrected
retroactively in Singular. Always validate data before calling
Singular.adRevenue().
Currency Handling
Ensure accurate currency reporting for multi-region apps and diverse ad networks.
- Verify Platform Currency: Check your mediation platform documentation for default currency (most use USD)
- Consistent Format: Always use uppercase three-letter ISO 4217 codes
- No Conversion: Report revenue in the currency provided by the ad network—do not convert currencies
- Currency Per Network: Different ad networks may report in different currencies—verify each separately
Platform-Specific Considerations
Handle platform differences in revenue reporting formats and units.
- AdMob Android: Revenue reported in micros—divide by 1,000,000 to convert to dollars
- AdMob iOS: Revenue reported in dollars—use value directly without conversion
- TradPlus: eCPM reported in milli-units—divide by 1,000 to convert to dollars
- AppLovin: Revenue reported in dollars—use value directly
- IronSource: Revenue reported in dollars—use value directly
Error Handling and Logging
Implement comprehensive logging for debugging and monitoring ad revenue tracking.
- Validation Failures: Log detailed error messages when validation fails, including actual values received
- Success Logging: Log successful revenue reports in development with platform, currency, and amount
- Production Monitoring: Use error tracking services (Sentry, Bugsnag) to monitor validation failures in production
- Revenue Anomalies: Alert on unusually high or low revenue values that may indicate integration issues
- Platform Coverage: Monitor which ad platforms are reporting revenue to ensure all are integrated correctly
Testing Strategy
Verify ad revenue tracking implementation before production deployment.
- Test Ads: Use test ad units from your mediation platform during development
- Validate Events: Check Singular dashboard for ad revenue events after test ad impressions
- Verify Currency: Confirm currency codes appear correctly in Singular reports
- Platform Accuracy: Ensure platform names are consistent and recognizable in reports
- Revenue Amounts: Verify revenue amounts match expected ranges for test ad units
- Multi-Platform: Test on both iOS and Android to verify platform-specific conversion logic
Performance Optimization
Minimize performance impact of ad revenue tracking on your app.
- Asynchronous Processing: Revenue callbacks execute asynchronously—no blocking of main thread
- Minimal Validation: Keep validation logic simple and fast (type checks, range checks)
- Batch Consideration: For high-volume apps, consider batching if supported by your analytics backend
- Error Handling: Use try-catch blocks to prevent revenue tracking errors from crashing your app
Verification and Troubleshooting
Verify Implementation
Confirm ad revenue data is flowing correctly to Singular.
- Enable Test Ads: Configure test mode in your mediation platform
- Trigger Impressions: Display test ads and trigger paid events
- Check Logs: Verify revenue tracking logs appear in console with correct values
- Dashboard Verification: Check Singular dashboard for ad revenue events (may take 15-30 minutes)
- Event Details: Verify currency, platform, and revenue amounts in Singular event details
Common Issues
- No Revenue Events: Verify paid event handlers are registered before loading ads and mediation SDK is initialized correctly
- Zero Revenue: Check platform-specific conversion logic (micros to dollars for Android AdMob, millis to dollars for TradPlus)
- Wrong Currency: Verify currency code matches what your mediation platform reports—check platform documentation
- Platform Name Mismatch: Use consistent platform names that match Singular's recognized platforms
- Missing Events: Ensure Singular SDK is initialized before ad revenue events occur
- Duplicate Events: Verify paid event handlers are registered only once, not on every ad load
Additional Resources: For detailed information, see the Ad Revenue Attribution FAQ and React Native SDK Methods Reference.