Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 15: Security and Fraud Prevention

Every purchase that flows through your app is a target. Attackers modify APKs to bypass purchase checks, replay stolen purchase tokens, abuse refund policies, and share accounts at scale. If your security model relies on the client alone, you have already lost. The Play Billing Library runs on the user's device, and anything running on the user's device can be tampered with.

This chapter gives you a practical, layered defense strategy for securing your billing implementation. You will learn how to move sensitive logic to your backend, verify every purchase through a 7 step workflow, protect your content from extraction, detect and respond to voided purchases, and defend against the most common attack vectors targeting Android apps.

Moving Sensitive Logic to Your Backend

The most important security decision you can make is this: never trust the client. Your Android app runs in an environment you do not control. Users can root their devices, install modified versions of your APK, intercept network traffic, and manipulate local storage. Any check that happens only on the device can be bypassed.

This means you should move all of the following to your backend server:

Purchase verification. When a purchase completes, the client should send the purchase token to your server. Your server calls the Google Play Developer API to confirm the purchase is real. The client never decides whether a purchase is valid.

Entitlement decisions. Your server maintains the authoritative record of what each user has access to. The client queries your server to find out what content to unlock. If the client stores entitlement state locally, treat it as a cache that your server can override at any time.

Acknowledgement and consumption. While the Play Billing Library supports client side acknowledgement, performing it on your server ties the acknowledgement directly to your entitlement grant. This guarantees you never acknowledge a purchase you have not fulfilled, and you never fulfill a purchase you cannot acknowledge.

Subscription state management. Subscription lifecycles are complex. Renewals, cancellations, grace periods, account holds, and pauses all generate state changes. Your server should process Real Time Developer Notifications (RTDNs) and maintain the current subscription state in your database. The client queries your server to determine access, not the Play Billing Library directly.

The general principle is straightforward: the client handles UI and user interaction, your server handles business logic and trust decisions. The client tells your server what happened (a purchase token was received), and your server decides what to do about it (verify, grant, acknowledge).

The 7 Step Purchase Verification Workflow

Purchase verification is the backbone of your security model. Every purchase that arrives from the client should pass through these seven steps on your backend before the user gets access to anything.

Step 1: Receive the Purchase Token from the Client

After a successful purchase, your app receives a Purchase object in the PurchasesUpdatedListener. Extract the purchase token and send it to your backend along with the user's account identifier:

kotlin
override fun onPurchasesUpdated(
    billingResult: BillingResult,
    purchases: List<Purchase>?
) {
    if (billingResult.responseCode ==
        BillingResponseCode.OK
    ) {
        purchases?.forEach { purchase ->
            if (purchase.purchaseState ==
                PurchaseState.PURCHASED
            ) {
                sendToBackend(
                    purchase.purchaseToken,
                    purchase.products,
                    currentUserId
                )
            }
        }
    }
}

Send the token over HTTPS with certificate pinning if possible. Include your own authentication token so your server knows which user is claiming the purchase.

Step 2: Call Google Play Developer API to Verify

Your backend uses the purchase token to query the Google Play Developer API. For one time products, call purchases.products.get. For subscriptions, call purchases.subscriptionsv2.get.

The API response contains the authoritative purchase record from Google. This is the source of truth. It tells you whether the purchase actually happened, what was purchased, what the current state is, and when it occurred.

If the API returns an error (such as a 404), the purchase token is invalid. Reject it immediately.

Step 3: Validate Response Fields

Once you have the API response, validate that the purchase matches what you expect:

Package name. Confirm the packageName in the response matches your app's package name. A valid purchase token from a different app should not grant access in yours.

Product ID. Confirm the productId (or subscription ID) matches what the client claimed was purchased. An attacker could send a token for a cheaper product and claim it was for a more expensive one.

Order ID. Store the orderId for your records. You will use it for refund tracking and customer support. Each unique transaction should have a unique order ID.

If any of these fields do not match the expected values, reject the purchase. Log the discrepancy for investigation.

Step 4: Check Purchase State

The API response includes a purchase state field. For one time products, the purchaseState field should be 0 (PURCHASED). For subscriptions, check the acknowledgementState and expiryTimeMillis.

Do not grant access for purchases in the PENDING state (value 2). Pending purchases mean the user has chosen a delayed payment method like cash at a convenience store. You will receive an RTDN when the payment completes.

For subscriptions, verify that the subscription has not expired and is not in a revoked state. The subscriptionState field from purchases.subscriptionsv2.get tells you the current lifecycle state.

Step 5: Check for Duplicate Tokens

Before granting entitlement, check your database for the purchase token. If this exact token has already been processed, do not grant a second entitlement. This prevents replay attacks where an attacker sends the same valid purchase token multiple times, possibly from different accounts.

Your database should have a unique constraint on the purchase token column. When a duplicate arrives, return the existing entitlement status rather than creating a new one. If the duplicate comes from a different user account than the original, flag it for investigation.

Step 6: Grant Entitlement

If the purchase passes all five checks above, grant the user access to the purchased content or feature. Write the entitlement to your database with the purchase token, user ID, product ID, order ID, and timestamp.

This is the only point at which the user should gain access. Everything before this step is validation. Everything after is cleanup.

Step 7: Acknowledge the Purchase

After granting entitlement, acknowledge the purchase through the Google Play Developer API on your server. For one time products, call purchases.products.acknowledge. For subscriptions, call purchases.subscriptions.acknowledge.

You must acknowledge within 3 days of the purchase. If you fail to do so, Google automatically refunds the purchase. Performing acknowledgement on your server, after entitlement grant, ensures these two actions stay in sync.

If the acknowledgement call fails, retry it with exponential backoff. A purchase that has been granted but not acknowledged will eventually be refunded, so treat acknowledgement failures as high priority.

Protecting Unlocked Content

Verification stops fake purchases. But what about the content itself? If your premium content is bundled inside the APK, anyone with a file extractor can pull it out without paying.

Never Bundle Premium Content in the APK

An APK is a ZIP file. Anyone can unzip it and access every resource, asset, and raw file inside. If your premium wallpapers, audio files, level data, or documents are sitting in the assets/ or res/ directories, they are free for the taking regardless of your purchase checks.

Instead, host premium content on your server and download it only after your server confirms the user has a valid entitlement. This keeps the content off the device until the user has paid for it.

For apps where offline access is important, download the content to internal storage (not external storage, which is readable by other apps) after purchase verification. Use Android's encrypted file storage or encrypt the files yourself with a key derived from a server provided secret.

Device Specific Encryption

When you must store premium content on the device, encrypt it with a key that is specific to both the user and the device:

  1. After purchase verification, your server generates an encryption key tied to the user's account and the device's unique identifier.
  2. Your server sends this key to the client over HTTPS.
  3. Your app encrypts the content with this key before writing it to disk.
  4. Your app requests the key from your server when it needs to decrypt the content.

This approach means that even if someone copies the encrypted files to another device, they cannot decrypt them without the correct key. If the user's entitlement is revoked, your server stops providing the key, and the content becomes inaccessible.

For the encryption itself, use AES-256-GCM through Android's javax.crypto APIs. Do not invent your own encryption scheme.

Detecting Voided Purchases

A voided purchase is one that was originally valid but has since been reversed. This happens when Google issues a refund, a chargeback occurs, or Google's fraud detection system flags the transaction. The user got access to your content, but the payment is no longer valid.

The Voided Purchases API

The Google Play Developer API provides the Voided Purchases API at purchases.voidedpurchases.list. This endpoint returns a list of purchases that have been voided for your app within a specified time range.

You should poll this endpoint regularly, at least once per day. For each voided purchase in the response:

  1. Look up the purchase token or order ID in your database.
  2. Identify the user who was granted the entitlement.
  3. Decide on the appropriate enforcement action (covered in the next section).

The API returns a voidedReason field that tells you why the purchase was voided:

  • 0 (Other): General void, no specific reason provided.
  • 1 (Remorse): The user requested a refund and Google granted it (buyer's remorse).
  • 2 (Not received): The user reported not receiving the purchased content.
  • 3 (Defective): The user reported the content was defective.
  • 4 (Accidental purchase): The user claimed the purchase was unintentional.
  • 5 (Fraud): Google's fraud detection flagged the transaction.
  • 6 (Friendly fraud): The user disputed the charge through their bank.
  • 7 (Chargeback): A chargeback was filed against the transaction.

The voidedSource field tells you whether the void came from the user (value 0) or from Google (value 1). This distinction matters for your enforcement decisions. A user requesting a single refund is different from a pattern of chargebacks flagged by Google.

Processing Voided Purchases at Scale

For apps with significant transaction volume, batch your voided purchase processing. Query the API with a pagination token to handle large result sets. Store the latest voidedTimeMillis value so your next query picks up where the last one left off.

Do not revoke access in real time as you discover each voided purchase. Batch the revocations and apply them during your regular entitlement sync. This reduces the chance of revoking access for a purchase that was voided by mistake (which does occasionally happen, and Google may reverse the void).

Graduated Enforcement for Fraud

Not every voided purchase means fraud. Users make legitimate refund requests, accidental purchases happen, and sometimes Google's fraud detection generates false positives. A good enforcement strategy is graduated, applying increasingly severe consequences based on the pattern of behavior.

Level 1: Warning

For a user's first voided purchase, especially one with a reason of "remorse" or "accidental purchase," take no enforcement action on the entitlement. Instead, log the event and flag the account for monitoring. If the user is currently subscribed through a different active purchase, there is nothing more to do.

If the voided purchase was the user's only entitlement, revoke access to the specific content but do not penalize the account. The user may have had a genuine issue.

Level 2: Disable Features

If a user accumulates multiple voided purchases within a short time window (for example, three voids within 30 days), begin restricting access more aggressively. Revoke all entitlements associated with voided purchases. You may also temporarily disable the ability to make new purchases within your app for that account.

At this stage, display a message explaining that purchase privileges have been restricted due to unusual refund activity, and direct the user to customer support if they believe this is an error.

Level 3: Revoke and Ban

For users with a clear pattern of abuse, such as repeated purchase and refund cycles, chargebacks, or purchases flagged as fraud by Google, revoke all entitlements and ban the account from future purchases. This is the appropriate response for confirmed fraud.

Store the device identifiers and account information associated with banned users so you can detect ban evasion. If a banned user creates a new account on the same device, your server can apply restrictions immediately.

Implementing Graduated Enforcement

Track each user's voided purchase history in your database:

  • Total number of voided purchases
  • Voided reasons and sources
  • Time span of voided purchases
  • Current enforcement level

Run your enforcement logic on your server during the voided purchase processing batch. Evaluate each user's history against your thresholds and apply the appropriate level. Log every enforcement action for auditing and customer support purposes.

The specific thresholds (how many voids trigger each level, the time windows, which void reasons are weighted more heavily) depend on your business. Start conservative, monitor the results, and adjust over time.

Helping Google Detect Fraud

Google has its own fraud detection systems that analyze purchase patterns across all apps. You can make these systems more effective by providing additional signals when launching the billing flow.

obfuscatedAccountId

The obfuscatedAccountId is a one way hash of your user's internal account identifier. You set it on the BillingFlowParams before launching the purchase:

kotlin
val flowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(
        listOf(productDetailsParams)
    )
    .setObfuscatedAccountId(
        hashAccountId(currentUser.id)
    )
    .build()

This allows Google to correlate purchases across sessions and devices for the same user. If a single account is making purchases from dozens of different devices, that is a fraud signal. Without the obfuscated account ID, Google can only correlate by Google account, which misses cases where attackers use multiple Google accounts.

Use a consistent, irreversible hash function (SHA-256 is a good choice) so the value cannot be reversed to identify the user but stays the same across sessions.

obfuscatedProfileId

The obfuscatedProfileId serves a similar purpose but at the profile level. If your app supports multiple profiles per account (common in streaming and gaming apps), set this to a hash of the active profile ID:

kotlin
val flowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(
        listOf(productDetailsParams)
    )
    .setObfuscatedAccountId(
        hashAccountId(currentUser.id)
    )
    .setObfuscatedProfileId(
        hashProfileId(currentUser.activeProfileId)
    )
    .build()

Both identifiers appear in the purchase record returned by the Google Play Developer API. You can use them on your backend to correlate purchases with your internal user records, even if the user switches Google accounts.

Set both identifiers on every purchase flow. They cost nothing to include and give both you and Google better visibility into purchase patterns.

Common Attack Vectors and Defenses

Understanding how attackers target in app purchases helps you build defenses that actually work. Here are the most common attack vectors and how to defend against each one.

Modified APKs Bypassing Client Side Checks

The attack. An attacker decompiles your APK, removes or modifies the purchase verification code, repackages the app, and distributes it. Users who install the modified APK get full access without paying. Some tools automate this process entirely, requiring zero technical skill from the end user.

The defense. If your purchase verification runs entirely on the client, this attack is trivial. The primary defense is server side verification, which renders client side tampering irrelevant. The modified APK can fake any local check it wants, but it cannot forge a valid response from the Google Play Developer API.

Additional layers of defense:

  • Google Play Integrity API. Call the Play Integrity API before or during the purchase flow. It tells your server whether the app binary is genuine and unmodified, whether the device passes basic integrity checks, and whether the app was installed from the Play Store. Reject purchases from devices that fail integrity checks.
  • Certificate pinning. Pin your server's TLS certificate in the app so attackers cannot intercept traffic between your app and your backend with a proxy.
  • Code obfuscation. Use R8 (or ProGuard) with aggressive obfuscation settings. This does not prevent decompilation, but it raises the effort required to understand and modify your code.

None of these secondary measures replace server side verification. They complement it.

Fake Purchase Tokens

The attack. An attacker sends fabricated or stolen purchase tokens to your backend, hoping your server will accept them and grant entitlements without proper validation.

The defense. Your 7 step verification workflow handles this. When your server calls the Google Play Developer API with a fake token, the API returns an error. When the server calls with a stolen token from a different app or product, the package name and product ID validation catches it. When the server sees a replayed token that was already processed, the duplicate check catches it.

The key is to never skip any of the seven steps. Each step catches a different type of invalid token.

Refund Abuse

The attack. A user purchases content, consumes or downloads it, and then requests a refund from Google. They keep the content and get their money back. Some users do this systematically, cycling through purchase and refund for every piece of premium content.

The defense. Poll the Voided Purchases API regularly and apply your graduated enforcement strategy. For consumable content (like in game currency), track what the user spent the currency on. If a purchase is refunded, deduct the equivalent amount from the user's balance. If the balance goes negative, restrict access until the balance is resolved.

For non consumable content, revoke access to the specific content associated with the refunded purchase. Your server should maintain a mapping between purchase tokens and the content they unlocked.

Google also provides the Play Console's "order management" section where you can block users from requesting refunds for your app. Use this for confirmed abusers.

Account Sharing

The attack. One user purchases a subscription and shares their account credentials with multiple people. Alternatively, they share a session token or authentication cookie. Instead of paying for five subscriptions, five people share one.

The defense. There is no perfect solution for account sharing, but you can limit its impact:

  • Concurrent device limits. Track the number of devices actively using each account. When the limit is exceeded, force a sign out on the oldest device or require re authentication.
  • Device fingerprinting. Record the devices associated with each account. If an account is used on an unusually high number of distinct devices over a short period, flag it for review.
  • Streaming/access restrictions. If your app delivers content in real time (streaming media, live data), limit concurrent streams per account.

The obfuscatedAccountId and obfuscatedProfileId you set during the purchase flow help here as well. If a single purchase token shows activity from many different device fingerprints, that is a signal of sharing.

Be careful not to punish legitimate use cases. A user may have a phone, tablet, and Chromebook. Your limits should accommodate normal multi device usage while catching abuse.

Timezone Manipulation

The attack. A user changes their device's timezone or date settings to exploit time based offers, extend free trials, or manipulate expiration checks. For example, setting the clock forward to skip a waiting period, or backward to extend a trial.

The defense. Never use the device clock for time sensitive decisions. Your server should be the source of truth for all time related checks:

  • Trial eligibility. Check trial eligibility on your server based on server timestamps, not client reported times.
  • Offer windows. Determine whether a promotional offer is active based on your server's clock.
  • Expiration checks. When the client asks whether a subscription is active, your server compares the subscription's expiryTimeMillis (from the Google Play Developer API) against the server's current time.

The Google Play Developer API returns all timestamps in server time (milliseconds since epoch, UTC). Use these values for your comparisons, never the device's System.currentTimeMillis() for business logic that determines access.

If your app must make offline time decisions (for example, caching access for a period when the device is offline), use a combination of server provided timestamps and monotonic clocks (SystemClock.elapsedRealtime()) that cannot be manipulated by changing the device's wall clock.

Security is not only about protecting your revenue from fraud. You also need to protect your app from legal risk related to intellectual property.

Protecting Your Own Content

If your app sells digital content such as images, music, text, or video, ensure you have the rights to distribute that content commercially. Licensing agreements with content creators should explicitly cover in app distribution on Android. Selling content you do not have distribution rights for exposes you to takedown requests and legal action, regardless of how well your billing implementation works.

Register trademarks for your app name, brand, and any distinctive product names. This gives you legal standing to pursue copycats who clone your app and sell it under a similar name, potentially stealing your customers and revenue.

Avoiding Infringement of Others' IP

Do not use trademarked names, logos, or copyrighted material in your product listings, subscription descriptions, or promotional materials without authorization. This includes:

  • Using another company's brand name in your product titles to attract searches.
  • Including copyrighted images or icons in your app's marketing materials.
  • Describing your product by reference to a competitor's trademarked product name.

Google enforces intellectual property policies on the Play Store. Violations can result in your app being suspended, your developer account being terminated, or both. An app suspension halts all billing and voids your active subscriber relationships, which is a far greater business impact than the original infringement may seem worth.

Dealing with Clones and Copycat Apps

If someone clones your app and sells it on the Play Store, you have several options:

  • DMCA takedown. File a DMCA takedown request through the Play Console's reporting tools. Google is required to respond to valid DMCA requests and will typically remove infringing apps.
  • Trademark complaint. If the clone uses your trademarked name or branding, file a trademark complaint through Google's legal channels.
  • Play Integrity as a signal. While not a direct anti piracy tool, the Play Integrity API can tell your server whether the requesting app was installed from the Play Store with your genuine signing certificate. Requests from apps with a different signature are clones.

Monitor the Play Store periodically for apps that mimic yours. Automated monitoring services can alert you when new apps appear with similar names, descriptions, or screenshots.

Want a simpler approach?

The RevenueCat SDK Handbook covers the same topics — with less code and a managed backend.

Related chapters

  • Chapter 9: Backend Architecture for Billing

    The 7-step server verification flow: receive token, call the API, check signatures, grant entitlements, acknowledge.

    Learn more
  • Chapter 6: The Purchase Flow

    End-to-end from launchBillingFlow() to acknowledgement: payment dialog, callbacks, pending purchases, and multi-quantity.

    Learn more
  • Chapter 16: Testing Your Integration

    License testers, Play Billing Lab, accelerated renewals, 11 test scenarios, and the response code simulator.

    Learn more