Google Play native integration
Connect your Google Play app to Affiliateo with the Play Developer API directly. No RevenueCat in the middle. Real-time notifications are optional: they make sales show up within seconds instead of within about 15 minutes.
When to use this
vs RevenueCat or Stripe
Pick this when
- You are fine creating a service account in a Google Cloud project
- You want to avoid third-party SaaS in your payment stack
- You only ship on Android, or you use Apple native or RevenueCat for iOS
RevenueCat is easier if
- You also ship on iOS (RC covers both stores with one integration)
- You don't want to manage a Google Cloud project and service account
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
Where the service account lives. Turn on the Google Play Android Developer API there. Turn on the Pub/Sub API too only if you want us to set up the optional real-time notifications for you.
Service account JSON key
Created in IAM and admin > Service accounts > Keys. We read your purchases with it, and Google signs the notifications it sends us with it.
Play Console access
Admin on the app in Play Console, to invite the service account.
Android package name
For example com.acme.app. It fills itself in from your Google Play link.
Setup
About 10 minutes. After that, Google can take a while to turn on new access.
Already using RevenueCat for this Android app? Use the same Google Cloud project. If you turn on notifications in step 6, pick the topic RevenueCat already made from the list, so our subscription sits beside RevenueCat's and both hear every notification. Play Console already sends to that topic, so you can skip the Play Console part of step 6.
affiliateo.If Play Console has not turned the new access on yet, the card says Waiting for Google. This can take up to 36 hours and is usually much faster. Tip: edit any product's description in Play Console, save, then change it back; this often speeds it up. We check every 10 minutes for 72 hours and notify you when it works, or you can press Check again yourself.
Setting notifications up by hand
google-play-developer-notifications@system.gserviceaccount.com with the Pub/Sub Publisher role.How attribution flows
The piece that ties an affiliate referral to a Google purchase.
The Affiliateo SDK gives every device a visitor id the first time your app opens, whether or not the person came from an affiliate link, and hands it to your app as obfuscatedAccountId. (Older SDK versions minted a separate UUID only for people from an affiliate link and registered it with us; purchases made under those keep working.)
At purchase time, your code passes the same UUID to Play Billing as obfuscatedAccountId. Google keeps it on that purchase and everything after it (the first sale, renewals, refunds), underexternalAccountIdentifiers.obfuscatedExternalAccountId.
Whenever we read a purchase from Google (from a real-time notification, from your app's purchase report, or from our own check when a period ends), we look up the visitor by that id in a single indexed query, tie the sale to the phone, and credit the affiliate when the person came from one. For attribution, setting the field at purchase is the only client work.
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. Google keeps it on this
// purchase and its renewals, which is how we find the buyer.
.apply {
Affiliateo.obfuscatedAccountId?.let { setObfuscatedAccountId(it) }
}
.build()
billingClient.launchBillingFlow(activity, params)
}Tagging purchases with your own account id
If your own server reads obfuscatedAccountId to know whose purchase it is, you can pass your own account id there instead of the SDK's. Then call identify() with that exact same id right after sign-in, before the person can buy: that is how we find the phone. Google takes any string up to 64 characters, but we match it to the identify() id exactly (only the upper or lower case of a UUID is ignored), and it has to be hard to guess: at least 8 characters, and not a plain number.
What we record
What each of Google's events records. They come from real-time notifications when those are on, and otherwise from our own checks with Google.
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_REVOKEDaccess endedMoves no money: the refund itself arrives as a voided purchaseONE_TIME_PRODUCT_PURCHASEDsale (one-time)Recorded with amount 0 (see limitations)ONE_TIME_PRODUCT_CANCELEDrefundOne-time purchase refundedVOIDED_PURCHASE (via voidedpurchases.list)refund (reconciliation)Read from Google daily, with or without real-time notificationsChanges that move no money are recorded as subscription changes, with no conversion: CANCELED, IN_GRACE_PERIOD and ON_HOLD (a failed payment), PAUSED, and EXPIRED (a trial that ended unpaid included). Without real-time notifications, we check each subscription with Google when its period ends, then 1 hour, 6 hours and 1 day later, and daily through Google's grace period and account hold. With them on, we still check 6 hours and 1 day after, in case a notification was lost.
Free access for affiliates
You can give an affiliate your paid app for free, for a length you pick. Apple and Google can't give anyone a subscription from outside your app, so your app asks us: pass the email to identify() when someone signs in, call freeAccess() every time the app opens, and unlock while it says active, next to your own purchase check. How free access works
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
"Waiting for the test notification" never finishes
Check that Play Console has the exact topic name the card shows, that the notification content is "Subscriptions, voided purchases, and all one-time products", and that you clicked Save changes before Send test notification. Set up by hand? Check that the subscription is Push, that its endpoint is ours, and that authentication uses the service account you connected.
Stuck on "Waiting for Google"
The service account is not invited in Play Console yet, or it lacks one of the three permissions. Check Users and permissions, then press Check again. After 72 hours we stop checking on our own, but Check again always works.
"Connect to Google" says a role or an API is missing
Grant the role it names to the service account in IAM and admin > IAM (Pub/Sub Admin, and Service Account User), or turn on the Pub/Sub API with the link it shows. If your organization uses Domain Restricted Sharing, Google Play cannot publish to your topics until an admin allows it. Until then, set notifications up by hand. Notifications are optional, so you can also leave them off.
"Needs attention": Google refused the key
The JSON key was deleted, or the service account was turned off. Create a new JSON key for the same service account, disconnect, and connect again with it. Nothing already recorded is lost; new sales and changes wait until then.
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. Passing your own account id instead? Check that identify() ran with that exact same id on the phone before the purchase.