Play's model: a service account and a notification topic
Each rail has its own platform-side shape, and knowing Play's up front prevents the most common "why isn't this working" question. Stripe connects with an OAuth click; Apple uploads .p8 keys; Google Play uses a service-account JSON key you upload, plus a Pub/Sub topic that Google publishes purchase notifications to.
One honest difference to expect: Play has no bulk history. Stripe lets Crossdeck list your past subscriptions; Google's stance is that a subscription belongs to the user-and-purchase-token relationship, not to an enumerable list, so Crossdeck can only fetch a purchase once it has a token to ask about. That means Play starts from a clean slate and fills in as purchases happen — expected, not a bug.
Service account, notifications, then one line at purchase
The dashboard walks you through the platform steps; the only code is the last one:
1 · Create a service account in Google Cloud, download its JSON key, and grant it access in Play Console (Users & permissions → the service account's client_email). Then upload the JSON in the Crossdeck dashboard. The key is a credential — Crossdeck stores it in Secret Manager.
2 · Wire Real-time Developer Notifications. In Play Console, point your app's RTDN topic at the Pub/Sub topic Crossdeck owns. Crossdeck receives Google's purchase notifications there.
3 · Tell Crossdeck who's buying. At purchase time, set the obfuscated account ID on the Play Billing flow to your user's ID — the same value you pass to identify():
// the same stable user ID you pass to identify()
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(/* … */)
.setObfuscatedAccountId(currentUserId)
.build()
billingClient.launchBillingFlow(activity, params)
The obfuscated account ID is Android's identity hint
setObfuscatedAccountId is Android's equivalent of Apple's appAccountToken — it stamps your user's ID onto the purchase. When Google's notification lands, Crossdeck re-fetches the purchase from the Play Developer API, reads the obfuscated account ID off it, and links the resulting customer to your existing developer user.
Skip it and the Play purchase still arrives, but with no hint of who bought it, so it attaches to a rail-only customer that has to be reconciled later. Setting it to the value you already pass to identify() is what closes that gap — the same identity, carried through every rail, so a buyer on Android lands on the very same person who bought on the web or in your iOS app.
The Play purchase lands on the right customer
Make a test purchase. Google's notification arrives, Crossdeck re-fetches the purchase, reads the account ID, and the subscription appears on the customer you identified — entitlement active, ready for your gate.
user_847's Play subscription, matched by obfuscatedAccountId — entitlement live. Same person, third rail.