Ad Revenue Attribution
Track ad revenue from mediation platforms and attribute it to the marketing campaigns that acquired your users, providing complete ROI visibility across campaign costs, in-app purchases, and advertising monetization.
Overview
What Is Ad Revenue Attribution
Ad Revenue Attribution connects mobile app advertising revenue to the user acquisition campaigns that drove app installs, enabling you to measure true campaign profitability including ad monetization.
Key Benefits:
- Unified ROI View: See campaign costs, in-app revenue, and ad revenue in a single dashboard
- Campaign Optimization: Send ad revenue data back to ad networks to improve bidding and targeting
- LTV Measurement: Calculate complete user lifetime value including advertising monetization
Data Sources: Ad revenue data typically comes from your mediation platform (e.g., AdMob, AppLovin MAX, IronSource) at either the user level or impression level. Singular supports multiple integration methods to receive this data.
Learn More: See the Ad Revenue Attribution FAQ for comprehensive details on setup, reporting, and troubleshooting.
Implementation Requirements
Critical Guidelines
Required SingularAdData parameters:
The constructor arguments
adPlatform
,
currency
, and
revenue
are all required. If any are missing, empty, or invalid, the SDK silently
drops the call to
Singular.adRevenue()
without raising an exception or returning an error.
Singular.adRevenue()
also silently no-ops when the SDK is not yet initialized — always confirm
Singular.init()
has run before calling it.
Data Accuracy Is Critical:
- Currency Codes: Use three-letter ISO 4217 currency codes (e.g., USD, EUR, INR). Many mediation platforms report in USD—verify your platform's currency before implementation
-
Validate Before Sending:
Always validate revenue
and currency data before calling
Singular.adRevenue(). Incorrect data cannot be corrected after submission - Platform Differences: Some SDKs report revenue in micros (divide by 1,000,000) while others report in standard currency units. Check your platform's documentation
Setup Steps
Follow these steps to implement ad revenue attribution:
- Update SDK: Ensure you're using the latest Singular SDK version
- Choose Integration: Select the mediation platform integration below that matches your setup
- Implement Callbacks: Add platform-specific paid event listeners to capture revenue data
- Validate Data: Test revenue reporting and verify data appears in Singular dashboard
Platform Integrations
AdMob Integration
Track ad revenue from Google AdMob using the paid event listener for impression-level revenue reporting.
Requirements:
- Enable paid event tracking in your AdMob account (see AdMob Support )
- Implement Google Mobile Ads SDK for Android (see Getting Started Guide )
Platform Revenue Reporting:
AdMob reports revenue
differently by platform. Android returns revenue in micros (e.g.,
$0.005 = 5000). Divide
adValue.valueMicros
by 1,000,000
to convert to standard currency units before sending to Singular.
Implementation
Set a paid event listener when loading ads to capture revenue data and send it to Singular.
import com.singular.sdk.Singular
import com.singular.sdk.SingularAdData
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
private const val AD_UNIT_ID = "YOUR_AD_UNIT_ID"
class AdManager(private val context: Context) {
private var rewardedAd: RewardedAd? = null
fun loadRewardedAd() {
val adRequest = AdRequest.Builder().build()
RewardedAd.load(context, AD_UNIT_ID, adRequest,
object : RewardedAdLoadCallback() {
override fun onAdLoaded(ad: RewardedAd) {
rewardedAd = ad
// Set full screen content callback
rewardedAd?.fullScreenContentCallback =
object : FullScreenContentCallback() {
override fun onAdShowedFullScreenContent() {
Log.d("AdManager", "Rewarded ad displayed")
}
override fun onAdDismissedFullScreenContent() {
Log.d("AdManager", "Rewarded ad dismissed")
}
}
// Set paid event listener for revenue tracking
rewardedAd?.setOnPaidEventListener { adValue ->
// Convert revenue from micros to standard units
val revenue = adValue.valueMicros / 1_000_000.0
val currency = adValue.currencyCode
// Validate revenue data before sending
if (revenue > 0 && !currency.isNullOrEmpty()) {
val data = SingularAdData(
"AdMob",
currency,
revenue
)
Singular.adRevenue(data)
Log.d("AdManager", "Ad revenue sent: $revenue $currency")
} else {
Log.w("AdManager", "Invalid revenue: $revenue $currency")
}
}
}
override fun onAdFailedToLoad(error: LoadAdError) {
Log.e("AdManager", "Ad failed to load: ${error.message}")
}
})
}
}
import com.singular.sdk.Singular;
import com.singular.sdk.SingularAdData;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.rewarded.RewardedAd;
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback;
private static final String AD_UNIT_ID = "YOUR_AD_UNIT_ID";
public class AdManager {
private RewardedAd rewardedAd;
private Context context;
public AdManager(Context context) {
this.context = context;
}
public void loadRewardedAd() {
AdRequest adRequest = new AdRequest.Builder().build();
RewardedAd.load(context, AD_UNIT_ID, adRequest,
new RewardedAdLoadCallback() {
@Override
public void onAdLoaded(RewardedAd ad) {
rewardedAd = ad;
// Set full screen content callback
rewardedAd.setFullScreenContentCallback(
new FullScreenContentCallback() {
@Override
public void onAdShowedFullScreenContent() {
Log.d("AdManager", "Rewarded ad displayed");
}
@Override
public void onAdDismissedFullScreenContent() {
Log.d("AdManager", "Rewarded ad dismissed");
}
});
// Set paid event listener for revenue tracking
rewardedAd.setOnPaidEventListener(new OnPaidEventListener() {
@Override
public void onPaidEvent(AdValue adValue) {
// Convert revenue from micros to standard units
double revenue = adValue.getValueMicros() / 1_000_000.0;
String currency = adValue.getCurrencyCode();
// Validate revenue data before sending
if (revenue > 0 && currency != null && !currency.isEmpty()) {
SingularAdData data = new SingularAdData(
"AdMob",
currency,
revenue
);
Singular.adRevenue(data);
Log.d("AdManager", "Ad revenue sent: " + revenue + " " + currency);
} else {
Log.w("AdManager", "Invalid revenue: " + revenue + " " + currency);
}
}
});
}
@Override
public void onAdFailedToLoad(LoadAdError error) {
Log.e("AdManager", "Ad failed to load: " + error.getMessage());
}
});
}
}
AppLovin MAX Integration
Share impression-level ad revenue using the AppLovin Impression-Level User Revenue API.
Requirements:
- Implement AppLovin MAX SDK (see Impression-Level Revenue API Guide )
-
Subscribe to the
max_revenue_eventstopic via AppLovin Communicator
Implementation
Handle revenue messages through the AppLovin Communicator callback.
import com.singular.sdk.Singular
import com.singular.sdk.SingularAdData
import com.applovin.communicator.AppLovinCommunicatorMessage
override fun onMessageReceived(message: AppLovinCommunicatorMessage) {
// Check for revenue events topic
if (message.topic == "max_revenue_events") {
val adData: Bundle? = message.messageData
// Extract and validate revenue value
val revenueValue = adData?.getDouble("revenue", 0.0) ?: 0.0
if (revenueValue > 0) {
val data = SingularAdData(
"AppLovin",
"USD", // AppLovin typically reports in USD
revenueValue
)
Singular.adRevenue(data)
Log.d("AppLovin", "Ad revenue sent: $revenueValue USD")
} else {
Log.w("AppLovin", "Invalid revenue value: $revenueValue")
}
}
}
import com.singular.sdk.Singular;
import com.singular.sdk.SingularAdData;
import com.applovin.communicator.AppLovinCommunicatorMessage;
@Override
public void onMessageReceived(AppLovinCommunicatorMessage message) {
// Check for revenue events topic
if ("max_revenue_events".equals(message.getTopic())) {
Bundle adData = message.getMessageData();
// Extract and validate revenue value
double revenueValue = (adData != null) ?
adData.getDouble("revenue", 0.0) : 0.0;
if (revenueValue > 0) {
SingularAdData data = new SingularAdData(
"AppLovin",
"USD", // AppLovin typically reports in USD
revenueValue
);
Singular.adRevenue(data);
Log.d("AppLovin", "Ad revenue sent: " + revenueValue + " USD");
} else {
Log.w("AppLovin", "Invalid revenue value: " + revenueValue);
}
}
}
Unity LevelPlay (IronSource) Integration
Track impression-level revenue from ironSource and mediated networks using the IronSource SDK.
Requirements:
- Implement ironSource SDK (see Getting Started Guide )
- Enable ARM SDK Postbacks flag in your IronSource dashboard
- Set impression data listener to receive revenue callbacks
Learn More: See IronSource Ad Revenue Documentation for complete setup details.
Implementation
Implement the impression data success callback to capture and send revenue.
import com.singular.sdk.Singular
import com.singular.sdk.SingularAdData
import com.ironsource.mediationsdk.impressionData.ImpressionData
fun onImpressionDataSuccess(impressionData: ImpressionData?) {
// Validate impression data
if (impressionData == null) {
Log.d("IronSource", "No impression data available")
return
}
// Extract and validate revenue
val revenue = impressionData.revenue?.toDouble() ?: 0.0
if (revenue <= 0) {
Log.w("IronSource", "Invalid revenue: $revenue")
return
}
// Create and send ad revenue data
val data = SingularAdData(
"IronSource",
"USD", // IronSource typically reports in USD
revenue
)
Singular.adRevenue(data)
Log.d("IronSource", "Ad revenue sent: $revenue USD")
}
import com.singular.sdk.Singular;
import com.singular.sdk.SingularAdData;
import com.ironsource.mediationsdk.impressionData.ImpressionData;
public void onImpressionDataSuccess(ImpressionData impressionData) {
// Validate impression data
if (impressionData == null) {
Log.d("IronSource", "No impression data available");
return;
}
// Extract and validate revenue
double revenue = (impressionData.getRevenue() != null) ?
impressionData.getRevenue().doubleValue() : 0.0;
if (revenue <= 0) {
Log.w("IronSource", "Invalid revenue: " + revenue);
return;
}
// Create and send ad revenue data
SingularAdData data = new SingularAdData(
"IronSource",
"USD", // IronSource typically reports in USD
revenue
);
Singular.adRevenue(data);
Log.d("IronSource", "Ad revenue sent: " + revenue + " USD");
}
TradPlus Integration
Capture ad revenue from TradPlus mediation using the global impression listener.
Requirements:
-
Set global impression listener via
TradPlusSdk.setGlobalImpressionListener() -
Handle
onImpressionSuccesscallback to receive revenue data - Convert eCPM from milli-units to standard currency (divide by 1000)
Implementation
Register a global impression listener to track all ad impressions and revenue.
import com.singular.sdk.Singular
import com.singular.sdk.SingularAdData
import com.tradplus.ads.mgr.GlobalImpressionManager
import com.tradplus.ads.base.bean.TPAdInfo
// Set global impression listener
TradPlusSdk.setGlobalImpressionListener(
object : GlobalImpressionManager.GlobalImpressionListener {
override fun onImpressionSuccess(tpAdInfo: TPAdInfo?) {
// Validate ad info
if (tpAdInfo == null) {
Log.w("TradPlus", "AdInfo is null")
return
}
// Convert eCPM from milli-units to dollars
val ecpm = tpAdInfo.ecpm ?: run {
Log.w("TradPlus", "eCPM is null")
return
}
val revenue = ecpm.toDouble() / 1000.0
// Validate revenue
if (revenue <= 0) {
Log.w("TradPlus", "Invalid revenue: $revenue")
return
}
// Create and send ad revenue data
val data = SingularAdData(
"TradPlus",
"USD",
revenue
)
Singular.adRevenue(data)
Log.d("TradPlus", "Ad revenue sent: $revenue USD")
}
}
)
import com.singular.sdk.Singular;
import com.singular.sdk.SingularAdData;
import com.tradplus.ads.mgr.GlobalImpressionManager;
import com.tradplus.ads.base.bean.TPAdInfo;
// Set global impression listener
TradPlusSdk.setGlobalImpressionListener(
new GlobalImpressionManager.GlobalImpressionListener() {
@Override
public void onImpressionSuccess(TPAdInfo tpAdInfo) {
// Validate ad info
if (tpAdInfo == null) {
Log.w("TradPlus", "AdInfo is null");
return;
}
// Validate eCPM is not null
if (tpAdInfo.getEcpm() == null) {
Log.w("TradPlus", "eCPM is null");
return;
}
// Convert eCPM from milli-units to dollars
double revenue = tpAdInfo.getEcpm().doubleValue() / 1000.0;
// Validate revenue
if (revenue <= 0) {
Log.w("TradPlus", "Invalid revenue: " + revenue);
return;
}
// Create and send ad revenue data
SingularAdData data = new SingularAdData(
"TradPlus",
"USD",
revenue
);
Singular.adRevenue(data);
Log.d("TradPlus", "Ad revenue sent: " + revenue + " USD");
}
}
);
Generic Integration (Other Platforms)
Integrate any mediation platform using the generic
SingularAdData
interface.
Requirements:
- Access to impression-level revenue data from your mediation platform
- Revenue amount in standard currency units (not micros)
- ISO 4217 currency code (e.g., USD, EUR, INR)
Data Accuracy: Validate revenue and currency data before sending to Singular. Incorrect data cannot be corrected after submission.
Implementation
Create a
SingularAdData
object with platform name, currency,
and revenue, then call
Singular.adRevenue()
.
import com.singular.sdk.Singular
import com.singular.sdk.SingularAdData
fun reportAdRevenue(adPlatform: String, currency: String, revenue: Double) {
// Validate revenue
if (revenue <= 0) {
Log.w("AdRevenue", "Invalid revenue: $revenue")
return
}
// Validate currency
if (currency.isEmpty()) {
Log.w("AdRevenue", "Invalid currency: $currency")
return
}
// Create ad revenue data object
val data = SingularAdData(
adPlatform,
currency,
revenue
)
// Send to Singular
Singular.adRevenue(data)
Log.d("AdRevenue", "Revenue sent: $revenue $currency from $adPlatform")
}
// Example usage
reportAdRevenue("MyMediationPlatform", "USD", 0.05)
import com.singular.sdk.Singular;
import com.singular.sdk.SingularAdData;
public void reportAdRevenue(String adPlatform, String currency, double revenue) {
// Validate revenue
if (revenue <= 0) {
Log.w("AdRevenue", "Invalid revenue: " + revenue);
return;
}
// Validate currency
if (currency == null || currency.isEmpty()) {
Log.w("AdRevenue", "Invalid currency: " + currency);
return;
}
// Create ad revenue data object
SingularAdData data = new SingularAdData(
adPlatform,
currency,
revenue
);
// Send to Singular
Singular.adRevenue(data);
Log.d("AdRevenue", "Revenue sent: " + revenue + " " + currency +
" from " + adPlatform);
}
// Example usage
reportAdRevenue("MyMediationPlatform", "USD", 0.05);
Enriching Ad Data with Additional Attributes
SingularAdData exposes chainable
setters for impression-level metadata. Use these to enrich the event
with placement, ad unit, mediation grouping, and precision details when
your mediation platform provides them. Attributes set by these methods
are surfaced in Singular reporting and downstream postbacks.
| Method | Description |
|---|---|
withNetworkName(String networkName)
|
Overrides the network name (defaults to the ad platform passed to the constructor). Use for mediation where the underlying network differs from the mediation platform. |
withAdType(String adType)
|
Ad format (for example,
"Rewarded"
,
"Interstitial"
,
"Banner"
).
|
withAdGroupType(String adGroupType)
|
Ad group categorization defined by your mediation platform. |
withImpressionId(String impressionId)
|
Unique identifier for the impression, used for deduplication and reconciliation. |
withAdPlacementName(String adPlacementName)
|
Human-readable placement name (for example,
"level_complete"
).
|
withAdUnitId(String adUnitId)
|
Platform-specific ad unit identifier (for example,
"ca-app-pub-..."
for AdMob).
|
withAdUnitName(String adUnitName)
|
Human-readable ad unit name. |
withAdGroupId(String adGroupId)
|
Ad group identifier from the mediation platform. |
withAdGroupName(String adGroupName)
|
Human-readable ad group name. |
withAdGroupPriority(String adGroupPriority)
|
Priority assigned to the ad group in the mediation waterfall. |
withPrecision(String precision)
|
Revenue precision indicator (for example,
"publisher_provided"
,
"estimated"
,
"exact"
) when supported by the platform.
|
withPlacementId(String placementId)
|
Platform-specific placement identifier. |
withLimitDataSharing(boolean shouldLimitDataSharing)
|
Overrides the global data-sharing limit for this single ad event, for example when a user has opted out of personalized advertising. |
Kotlin example:
val data = SingularAdData("AdMob", "USD", 0.05)
.withAdUnitId("ca-app-pub-123456789/1234567890")
.withAdType("Rewarded")
.withAdPlacementName("level_complete")
.withImpressionId("imp_abc123")
.withPrecision("publisher_provided")
Singular.adRevenue(data)
Java example:
SingularAdData data = new SingularAdData("AdMob", "USD", 0.05)
.withAdUnitId("ca-app-pub-123456789/1234567890")
.withAdType("Rewarded")
.withAdPlacementName("level_complete")
.withImpressionId("imp_abc123")
.withPrecision("publisher_provided");
Singular.adRevenue(data);
Testing and Validation
Verify Revenue Reporting
Test your ad revenue implementation to ensure data flows correctly to Singular.
- Check Logs: Verify revenue callback logs appear with correct values and currency
- Test Ads: Load and display test ads to trigger revenue events
- Dashboard Verification: Confirm revenue appears in Singular dashboard within 24 hours
- Data Accuracy: Validate revenue amounts match your mediation platform reports
Troubleshooting: If revenue doesn't appear in Singular, check that:
- Ad revenue attribution is enabled in your Singular account
- Revenue values are greater than 0
- Currency codes are valid ISO 4217 codes
- Platform name matches Singular's expected format