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.

1
Create the service account. In Google Cloud Console, open IAM and admin > Service accounts > Create service account. Name it something like affiliateo.
2
Optional: give it two roles for notifications. Only needed if you want us to set up the real-time notifications in step 6 for you. In IAM and admin > IAM, grant the service account Pub/Sub Admin (to create the topic and let Google Play publish to it) and Service Account User (so Google can sign the notifications with it). Skip this if you leave notifications off or set them up by hand.
3
Create a JSON key. Open the service account > Keys > Add key > Create new key > JSON. A file downloads. Keep it safe.
4
Invite it in Play Console. In Play Console , open Users and permissions > Invite new users. Paste the service account's email and give it, for this app: View app information, View financial data, and Manage orders and subscriptions. Affiliateo shows this email with a Copy button as soon as you drop the key.
5
Connect in Affiliateo. Drop the .json file into the box, click the box to choose it, or paste the key. The package name fills in from your Google Play link. Click Connect Google Play. We check that Google accepts the key, that it can read your purchases and refunds, and that it can read your products and subscriptions.

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.

6
Optional: turn on real-time notifications. Without them, we check each subscription with Google when its period ends (the check runs every 15 minutes). That records renewals, trials that turn paid or end unpaid, failed payments, cancellations, expiries and pauses. Refunds are read daily. A new purchase is found when your app reports it with the SDK's Affiliateo.purchase call (purchase() in React Native): we look up its order id with Google, using the View financial data permission from step 4. With notifications on, all of this shows up within seconds, and we also hear about new purchases your app did not report. To turn them on, choose a topic on the connected card (an existing one, or Create a new topic) and click Connect to Google. We create the topic if needed, let Google Play publish to it, and add our push subscription. Then, in Play Console > Monetize with Play > Monetization setup, paste the topic name under Real-time developer notifications, choose "Subscriptions, voided purchases, and all one-time products", click Save changes, then Send test notification. The card then says Notifications working.

Setting notifications up by hand

1
In Pub/Sub, create a topic, or use RevenueCat's.
2
On the topic, add google-play-developer-notifications@system.gserviceaccount.com with the Pub/Sub Publisher role.
3
Create a push subscription on the topic. Its endpoint is the push endpoint shown on the card under Set it up by hand. Turn authentication on, pick the service account you connected, and leave the audience empty.
4
Paste the full topic name (projects/your-project/topics/your-topic) in Play Console, Monetization setup, and send a test notification.

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

PurchaseManager.kt
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 sub
SUBSCRIPTION_RENEWEDrenewalLocks to original affiliate + commission rate
SUBSCRIPTION_RECOVEREDrenewalAccount hold cleared; treat as renewal
SUBSCRIPTION_RESTARTEDrenewalUser re-enabled auto-renew after canceling
SUBSCRIPTION_REVOKEDaccess endedMoves no money: the refund itself arrives as a voided purchase
ONE_TIME_PRODUCT_PURCHASEDsale (one-time)Recorded with amount 0 (see limitations)
ONE_TIME_PRODUCT_CANCELEDrefundOne-time purchase refunded
VOIDED_PURCHASE (via voidedpurchases.list)refund (reconciliation)Read from Google daily, with or without real-time notifications

Changes 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

One-time IAP price isn't in Google's API response. 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.
Currency reporting is native, not normalized. Each conversion stores the price in the actual currency Google charged in. Cross-currency aggregate stats use raw amounts (no FX conversion). Single-currency apps are unaffected.
Pre-existing purchases aren't retroactively attributed. Customers who installed before you wired up the SDK can't carry an obfuscatedAccountId. Those purchases land as organic (no commission) until they purchase again with the SDK active.

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.