Skip to main content
Supported on: Android SDK 1.0.33+
The AppDNA billing module provides a unified interface for in-app purchases, subscription management, and entitlement checking on Android. It integrates with Google Play Billing Library 7.0.0 and handles purchase acknowledgment, restore, and entitlement state synchronization automatically.

Configuration

The Android SDK defaults to Google Play Billing, but the billing backend is selectable via the billingProvider configuration option (default BillingProvider.StoreKit2, which maps to Google Play Billing on Android). You enable billing by calling AppDNA.configure(...) from your Application.onCreate() and ensuring the Play Billing dependency is on your classpath (the SDK pulls it in transitively).
BillingProvider is a sealed class with these cases:
  • BillingProvider.StoreKit2 — the default (omitted = StoreKit2): AppDNA’s own billing against Google Play.
  • BillingProvider.RevenueCat — carries no key; RevenueCat is your purchase SDK and owns every purchase.
  • BillingProvider.Adapty(apiKey) — Adapty is your purchase SDK and owns every purchase. When the Adapty SDK is on the classpath, AppDNA activates it with this key — Adapty.activate(context, AdaptyConfig) (Adapty 2.11 and later, the only form in 4.x), or Adapty.activate(context, apiKey) on an older 2.x; do not activate Adapty yourself as well, or it is activated twice. An empty key is refused: Adapty is not activated and an error is logged.
  • BillingProvider.None — billing disabled; purchases and restores fail with providerNotAvailable (“No billing provider configured”).

Who owns a purchase

Exactly one component finishes a store transaction. On Android the SDK acknowledges (or consumes) a Play purchase only when billingProvider is StoreKit2: Choose RevenueCat when RevenueCat is your purchase SDK. AppDNA then never finishes or acknowledges a purchase; a tap on a plan in an AppDNA paywall calls onPaywallPurchaseFailed(errorType = "providerNotAvailable", productId = …) (and emits purchase_failed with error_type: "providerNotAvailable") without opening the Play sheet; and purchases reach AppDNA through the RevenueCat webhook. Recipe: catch that callback, start the purchase with RevenueCat yourself (Purchases.sharedInstance.purchase(...)), then dismiss the paywall. Adapty behaves the same on Android: start the purchase with Adapty from the callback.
The SDK’s internal RevenueCat and Adapty bridge classes are not usable on Android — they are never used for purchases, and their forwarding methods are not public (the Adapty bridge only activates Adapty with your key). Under RevenueCat / Adapty the SDK only reads your Play purchases locally (read-only entitlements) and never writes to the store. Use the provider’s own entitlement API (CustomerInfo for RevenueCat, Adapty’s profile access levels) as the source of truth; the SDK’s store-derived entitlements can differ (e.g. promotional grants). If the provider does not acknowledge a purchase within 3 days, Play refunds it — that is the provider’s contract.
The SDK sends billing_owner: "sdk" with every verify / restore call (only StoreKit2 makes these calls). An app with a connected RevenueCat or Adapty integration that still runs an older Android SDK (1.0.53 or earlier) on StoreKit2 gets its verifications refused (409 provider_owned) until it updates, because the old SDK does not say who owns the purchase.
Adapty webhook. Connect the Adapty webhook (Inbound integrations) to add server-side renewal and refund data once its mapping is enabled; the SDK keeps sending the device renewal / cancel events.

Module Access

Access the billing module through the AppDNA.billing property:

Get Products

Retrieve product details for one or more product IDs (suspend function — call from a coroutine, or use the Future overload from Java):
Java-friendly variant: AppDNA.billing.getProductsFuture(productIds): CompletableFuture<List<ProductInfo>>.

ProductInfo

Purchase a Subscription

Initiate a purchase flow for a specific product. purchase is a suspend function that returns a TransactionInfo on success and requires the current Activity to host the Play Billing flow:
The Google Play purchase result also fires through your registered AppDNABillingDelegate (see below).
Pass the foreground Activity directly — the SDK uses it to launch Google Play’s billing activity. Calling from a Service or Application context throws.

Promotional offers

Pass a PurchaseOptions to select a specific subscription offer token:
Server-side receipt verification is performed automatically by the SDK. You do not need to send purchase tokens to your own server for validation. With billingProvider: StoreKit2, Android purchases need Google Play credentials in AppDNA (else Play refunds them after 3 days) — see Google Play credentials and notifications. Apps whose billingProvider is RevenueCat or Adapty do not add them.
A custom PurchaseOptions.appAccountToken is supported: the purchase is verified and granted to the signed-in caller unless another user already owns it.

Consumables

Mark consumable products as Consumable in the console (Monetization → Products). The SDK consumes a consumable immediately after it is verified and reported, so it can be bought again at once. purchase_completed carries is_consumable. A consumable whose verification failed during the purchase and that is completed later (app start, identify, restore) is delivered through onPurchaseCompleted — at once when a delivering billing delegate is set (setDelegate(delegate), or setDelegate(delegate, deliversPurchases = true)), otherwise queued and delivered when one is set or after identify. Grant idempotently by transactionId (a crash can re-deliver once). The queue keeps 100 entries for 30 days; past either, a manual grant from the dashboard / support is needed. Known: TransactionInfo has no quantity (a quantity above 1 under-grants), and after a reinstall an earlier acknowledged purchase is not reported again.

Purchase errors

A failed AppDNA.billing.purchase throws one of the exceptions below (the Java purchaseFuture completes exceptionally with it). Call billingErrorType(e) to map any of them to one of userCancelled, productNotFound, verificationFailed, networkError, serverError, pending, providerNotAvailable, unknown:
  • providerNotAvailable — billingProvider is RevenueCat, Adapty or None (BillingError.ProviderNotAvailable; under None the message is “No billing provider configured”). Start the purchase with your provider instead.
  • verificationFailed — AppDNA’s server could not verify the purchase; purchase_failed.reason carries the server’s code (for example store_credentials_missing) when present.
  • A purchase started while the Play connection is failing fails within 30 s with serverError.
  • purchase / restorePurchases called before configure has set billing up, or after AppDNA.shutdown(), fail with unknown and the message “AppDNA SDK not configured yet — call configure() first”. A paywall purchase tap in the same window — before billing is ready (including between configure() and the end of billing setup) or after shutdown() — fails the same way: one purchase_failed with reason: "not_configured" and error_type: "unknown", and onPaywallPurchaseFailed with errorType == "unknown" (not providerNotAvailable). A paywall restore tap in that window fails with the same unknown error (onPaywallRestoreFailed, one purchase_restore_failed with reason: "not_configured"), without onPaywallRestoreStarted. See A tap before billing is ready.
  • item_already_owned — a re-tap of an owned item that could not be completed; see Re-buying an owned item.

Restore Purchases

Restore previously purchased subscriptions and one-time products (e.g., after a device change or reinstall). Suspend function — returns List<String> of restored product IDs:
Restoring purchases queries Google Play for all active subscriptions and updates entitlements accordingly. This is useful after a reinstall or device change.

Restore can fail

restorePurchases() throws on failure (the Java restorePurchasesAsync() completes exceptionally). Entitlements are left unchanged, onRestoreCompleted does not fire, and purchase_restore_failed is tracked once billing has initialised (a call before that throws the “AppDNA SDK not configured yet” unknown error untracked; a CancellationException from your own cancelled coroutine is rethrown untracked). The error is a billingErrorType value: reset() and identify with a user different from the one already identified clear the cached entitlements (the first identify after an anonymous session keeps them); they come back from the next refresh or entitlement update.

Entitlements

Retrieve the current user’s entitlements (suspend function):

Entitlement

Check Active Subscription

Quickly check if the user has any active entitlement. hasActiveSubscription() counts active one-time products (a lifetime unlock) as well as subscriptions, on both platforms:

Listen for Entitlement Changes

Register a listener to be notified when entitlements change (e.g., subscription renewal, expiration, or new purchase):

AppDNABillingDelegate

All 5 methods on this delegate fire from the active billing bridge (Google Play NativeBillingManager). Register via AppDNA.billingDelegate = yourHandler.
  • onPurchaseCompleted and onPurchaseFailed fire for every purchase regardless of entry point — paywall-driven OR direct AppDNA.billing.purchase(...) calls.
  • onPurchaseCompleted may also fire later, at least once — at app start, on setDelegate, or after identify — for a purchase whose verification failed earlier or that was made outside the app (a Play Store resubscribe, a promo code, a pending purchase that completed while the app was closed). Such a report has no paywall (paywall_id is ""). Grant idempotently by transaction.transactionId.
  • onRestoreCompleted fires after every successful restore.
  • onEntitlementsChanged fires whenever entitlements change (also broadcast via the local EntitlementCache).
  • onBillingUnavailable fires once when the billing connection gives up after exhausting all reconnect attempts (Play Services missing or permanently broken). Hide paywalls / disable purchase UI when this fires.
Overlap with AppDNAPaywallDelegate: when a purchase comes from a paywall, both AppDNAPaywallDelegate.onPaywallPurchaseCompleted AND AppDNABillingDelegate.onPurchaseCompleted fire. Each represents a different layer (paywall lifecycle vs platform billing). Pick one as your source of truth for your analytics — they will not double-count if you only listen on one. For more granular control, implement the AppDNABillingDelegate interface:
All methods have default empty implementations — override only the ones you need.

Late purchases and the delivery queue

Purchase reports that did not resolve a live purchase() call (rescued purchases, pending purchases approved later, a cancelled purchase() call) are kept in a durable queue and delivered through onPurchaseCompleted when a billing delegate is set, after identify, or once billing has initialised during configure’s background bootstrap. Any billing delegate drains the queue, so implement onPurchaseCompleted — or register a delegate that should not receive queued purchases with:
The one-argument setDelegate(delegate) is kept and delivers queued purchases. A callback that returns (even after its timeout) counts as delivered.

Example Implementation

Data Types

TransactionInfo

PurchaseResult

The PurchaseResult sealed class represents all possible outcomes of a purchase:

Auto-Tracked Events

The billing module automatically tracks the following events:
These events include the product identifier and relevant metadata. They are tracked regardless of whether you implement AppDNABillingDelegate.
Under billingProvider: StoreKit2 the SDK acknowledges purchases (and consumes consumables) after server verification, as Google Play requires within 3 days; an unacknowledged purchase is refunded by Play. Under RevenueCat / Adapty / None the SDK never acknowledges. Re-tapping an owned non-consumable or subscription emits one purchase_restored with reason: "item_already_owned" (no price), not a second conversion.

Re-buying an owned item

When Play answers a purchase with “item already owned”, the SDK looks at the owned purchase for that product (it never runs a full restore): Do not grant from either failure. A purchase whose verification failed is retried at the next return to the foreground (app start included), identify, restore or refreshEntitlementCache() (see Consumables for how a late completion is delivered). A consumable that was consumed silently is bought again once while the purchase screen is still open; if the sheet cannot be relaunched, the purchase also ends Failed("item_already_owned") with the same purchase_failed.

Google Play credentials and notifications

Needed when AppDNA handles purchases (billingProvider: StoreKit2); apps on RevenueCat or Adapty do not add Play credentials.
  1. In the console open Settings → Store Credentials, and in the Google Play Console card paste your Google Cloud service account (with access to your app in the Play Console) into Service Account JSON and your app’s Package Name.
  2. Click Save Credentials; the page then shows the Google Play Webhook URL — copy it.
  3. In Google Cloud → Pub/Sub, create a topic for Play Real-time developer notifications, select it in the Play Console (Monetization setup), and add a push subscription on that topic whose endpoint is the AppDNA webhook URL, with authentication enabled.
Secure your Play notifications. Find the service account of the Pub/Sub push subscription that targets the AppDNA webhook URL and paste it into Settings → Store Credentials (Google Play Console card) → “Push service account”; if your subscription sets a custom audience, paste it into “Push audience (optional)”, then click Save push authentication. These fields appear after the credentials are saved, and only the organisation owner can change them. Once a push service account is saved, AppDNA rejects (401) a notification whose Pub/Sub token was not issued for that service account, or whose audience is neither the webhook URL nor the push audience you entered. Until then a notification is accepted with any valid Google-signed Pub/Sub token (a missing or invalid token is still rejected), a warning is logged, and the page shows a warning banner. Free-trial detection. AppDNA marks a free-trial purchase (is_trial: true, price 0) from the subscription’s current offer phase reported by Play (offerPhase). A fallback that reads the offer’s first phase through Play’s monetization API is not enabled; it would need the “View app information” permission for the service account and would degrade gracefully on a permission error. Known: a purchase reported after its trial ended is reported with is_trial: false at full price.

Full Example

Next Steps

Revenue the SDK cannot see

Supported on: Android SDK 1.0.55+
If some of your revenue never passes through the SDK — billing run by your own server, a web checkout, a seat sold by a salesperson — those users are invisible to the paying-user meter unless you say otherwise:
Safe to call on every launch; a user counted by both a purchase and a report is billed once. See Who counts as a paying user for what counts, what it costs and the rules we hold ourselves to.