Google Play native integration
Connect your Google Play app to Affiliateo using Real-Time Developer Notifications (RTDN) and the Play Developer API directly. No RevenueCat in the middle.
When to use this
vs RevenueCat or Stripe
Pick this when
- You already operate a GCP project and are comfortable with Pub/Sub setup
- You want to avoid third-party SaaS in your payment stack
- You only ship on Android (or you're combining with Apple native for iOS)
RevenueCat is easier if
- You also ship on iOS (RC covers both stores with one integration)
- You don't want to maintain a GCP project + Pub/Sub configuration
Don't use this for
- Physical goods / services billed outside Play (use Stripe)
- Web purchases (Play Billing is Android-only)
What you'll need
Google Cloud project
Owned by you, with Pub/Sub enabled. Free tier covers RTDN volume.
Service account JSON key
Created in IAM → Service Accounts → Keys. Grants both Play Developer API access AND Pub/Sub push identity.
Play Console access
Owner / Admin on the app's Play Console listing, to grant the SA permissions and enable RTDN.
Android package name
e.g. com.acme.app. Must match what's live in Play Console.
Setup
Roughly 15 minutes once GCP + Play Console are open.
affiliateo-rtdn. Skip the optional role grants on this screen.affiliateo-rtdn@your-project.iam.gserviceaccount.com), grant App access > View financial data and View app information on the app you're connecting.play-rtdn. Topics are free; you'll only pay for delivery volume past the free tier.google-play-developer-notifications@system.gserviceaccount.com with role Pub/Sub Publisher. This is what lets Google publish RTDN into your topic.projects/{proj}/topics/{name}), and click Connect Google Play. We'll call voidedpurchases.list to validate the key works and the SA has the right access.- Delivery type: Push
- Endpoint URL: paste the URL Affiliateo gave you after Connect
- Enable authentication: ON
- Service account: pick the same SA you uploaded
- Audience: leave blank (defaults to the endpoint URL, which is what we expect)
projects/{proj}/topics/{name}). Click Send test notification. Within a minute you should see Affiliateo's connection card flip from "Awaiting first RTDN" to "Receiving RTDN".How attribution flows
The piece that ties an affiliate referral to a Google purchase.
The Affiliateo SDK mints a stable UUID v4 the first time a user opens your app from an affiliate link. The UUID is persisted on-device so it survives launches, and registered with Affiliateo via POST /api/v1/mobile/google-account-id so the server can map it to the visitor/affiliate.
At purchase time, your code passes the same UUID to Play Billing as obfuscatedAccountId. Google stamps it onto every subsequent RTDN for that purchase chain (initial sale, renewals, refunds) underexternalAccountIdentifiers.obfuscatedExternalAccountId.
When the RTDN arrives, we look up the visitor by UUID in a single indexed query and credit the affiliate. No additional client work beyond setting the field at purchase.
Purchase code
Pass the UUID to Play Billing.
Pass obfuscatedAccountId to BillingFlowParams at purchase time
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.ProductDetails
import com.affiliateo.Affiliateo
fun launchBilling(activity: Activity, product: ProductDetails) {
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(product)
.build()
))
// The UUID Affiliateo minted on identify. Stamped onto every RTDN
// we receive from Google for this user's purchases + renewals.
.apply {
Affiliateo.obfuscatedAccountId?.let { setObfuscatedAccountId(it) }
}
.build()
billingClient.launchBillingFlow(activity, params)
}What we record
RTDN types that produce a conversion row.
SUBSCRIPTION_PURCHASEDsale (subscription)First payment for an auto-renewing subSUBSCRIPTION_RENEWEDrenewalLocks to original affiliate + commission rateSUBSCRIPTION_RECOVEREDrenewalAccount hold cleared; treat as renewalSUBSCRIPTION_RESTARTEDrenewalUser re-enabled auto-renew after cancelingSUBSCRIPTION_REVOKEDrefundAdmin / customer support refundONE_TIME_PRODUCT_PURCHASEDsale (one-time)Recorded with amount 0 (see limitations)ONE_TIME_PRODUCT_CANCELEDrefundOne-time purchase refundedVOIDED_PURCHASE (via voidedpurchases.list)refund (reconciliation)Daily backfill in case live RTDN was missedOther RTDN types (state changes that don't affect money: ON_HOLD, IN_GRACE_PERIOD, EXPIRED, etc.) are deduped at the inbox level but skipped for conversion side effects.
Known limitations
purchases.products.get returns purchase state but no monetary amount. We record the conversion with amount_cents=0 and no commission. Subscriptions are unaffected. If you ship one-time IAPs and want them attributed for commission, ping us and we'll wire up a price-lookup workaround.Troubleshooting
Connection shows "Awaiting first RTDN" indefinitely
The most common cause is the Pub/Sub subscription not being configured to push to us. In Cloud Console → Pub/Sub → Subscriptions, confirm: delivery is Push, endpoint URL matches what we returned, authentication is enabled with the service account you connected. Then in Play Console, click Send test notification again.
Health banner: "Google credentials invalid"
Your service account key was revoked or expired. In GCP, generate a new JSON key for the same SA and reconnect in the app edit form. Existing data isn't lost; only future RTDN delivery is paused.
Sale didn't attribute to the expected affiliate
The purchase's obfuscatedAccountId didn't match a tracked visitor on this app. Either the SDK wasn't initialized before purchase, the user installed before the SDK was wired up, or the affiliate match window (30 days) expired. Check the visitor row for the user's device id.