Data quality ยท troubleshooting

How many of my iOS app users were pirated copies

Two of my four live apps have a large share of users running cracked copies. The numbers, how Apple's AdServices framework gives those copies away, what they do to active users, conversion and funnels, and what you can do about it.

Razvan Popa, runs four live iOS apps · · 

Short answer: from mid-July to September 23, 2026, 162 of the 1,026 new users of LuminaClean, my photo cleaner, were pirated copies (about 16%), and so were 51 of the 123 new users of BookBinge, my reading tracker (41%). None of them ever paid. You can spot these copies with Apple's AdServices framework: a copy that wasn't installed from the App Store gets Apple's documented test payload (campaignId 1234567890) instead of a real attribution answer. Mark your own test devices first: Apple ties the payload to Developer Mode, which is on for every phone you run Xcode builds on. Left in, pirated users inflate active-user counts and pull every per-user conversion rate down by their share.

My apps' analytics SDK asks Apple's AdServices framework, once per install, whether the install came from an Apple Ads campaign. For a sizable share of users, Apple answers with the placeholder payload it documents as test data. In my data that payload marks copies of the app that didn't come from the App Store. The numbers below come from FolioKit, the analytics tool I build and run my apps on, and cover the period from mid-July, when tracking started, to September 23, 2026.

The numbers from two apps

"New users" means people first seen after tracking started. Users who had the app before the SDK shipped and then updated aren't counted as new, and my own test devices are excluded.

AppNew usersPirated copiesShareEver paid
LuminaClean (photo cleaner)1,026162about 16%0 of 162
BookBinge (reading tracker)1235141%0 of 51

My other two apps, Inkfall (an arcade game) and TicFlow (a habit and tic tracker), had 3 pirated users each. That's too few to turn into a rate.

The share moves from month to month:

Month first seenLuminaCleanBookBinge
July22.6% of 35951.7% of 29
August10.1% of 50643.8% of 64
September (to the 23rd)18.6% of 16126.7% of 30

BookBinge's monthly samples are small (29 to 64 new users), so read its column as rough. LuminaClean's larger months still swing between 10.1% and 22.6%, which matters later: a per-user rate can move by that much with no change in how real users behave.

What pirated users did inside the apps over the last 30 days:

Last 30 daysLuminaCleanBookBinge
Share of all events from pirated copies10.7% of 29,91317.3% of 1,886
Share of paywall views from pirated copies0.0% of 35717.5% of 63

The paywall row is the odd one. Pirated LuminaClean users produced a tenth of all events and not one of the 357 paywall views, so the cracked copy apparently has the paywall removed. The cracked BookBinge copy still shows it.

Where the pirated copies are

Country comes from the IP address when events arrive (the address itself isn't stored). The pirated users are far more concentrated than the real ones:

  • LuminaClean, pirated copies: Saudi Arabia 63.0% (102 of 162), then the US 5.6%, the UAE 4.9%, Iraq 3.7% and Egypt 3.7%.
  • LuminaClean, real users: the largest country is Australia at 8.7%, then Canada at 8.6%.
  • BookBinge, pirated copies: Saudi Arabia 39.2% (20 of 51).

I haven't looked into where the copies are shared, and I won't guess. The practical point is what those 102 users do to an unfiltered country chart: they make Saudi Arabia look like one of LuminaClean's biggest markets. If you pick localizations or ad countries from that chart, you'd be choosing for people who don't pay.

How to detect pirated copies with AdServices

The check piggybacks on Apple Ads attribution, which many apps already run. On first launch the app asks the AdServices framework for a token (AAAttribution.attributionToken()) and posts it to Apple's attribution API; the full flow, retries included, is in Apple Ads attribution without an MMP. Normally the answer is a real campaign or "attribution": false. Apple's AdServices reference also shows a test payload and says "AdServices will return a test payload when developer mode is on". Its identifying fields look like this:

{
  "attribution": true,
  "orgId": 1234567890,
  "campaignId": 1234567890,
  "adGroupId": 1234567890,
  "keywordId": 123222,
  "conversionType": "Download",
  "clickDate": "2020-04-08T17:17Z"
}

Those IDs are placeholders, so the check is one comparison after you decode the response:

// data = the body Apple's attribution API returned
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let campaignId = (json?["campaignId"] as? NSNumber)?.int64Value
let isTestPayload = (campaignId == 1234567890)

Store the result on the user record, not as a filter you remember to add to each query. Every metric should leave these users out by default.

Why a cracked copy gets test data: Apple's page on enabling Developer Mode says the setting is needed to run development-signed software, such as an app built from Xcode or an .ipa installed by hand, and plays no part in App Store or TestFlight installs. A cracked copy is an .ipa re-signed and installed outside the App Store. That's my reading of why the signal works; Apple doesn't describe it as a piracy check.

What the check catches by mistake, and what it misses

  • Your own devices. Apple ties the test payload to the device's Developer Mode setting, not to how the app was installed, and Developer Mode is on for any phone you run Xcode builds on. On those phones every build can come back as test data, including TestFlight and App Store installs; a TestFlight tester without Developer Mode gets normal answers. Mark your own devices as internal before counting anything; I mark mine by user ID. At runtime, StoreKit can tell you as well: Apple's sandbox testing guide says apps downloaded from TestFlight always run in the sandbox environment, and AppTransaction reports that environment (see the second check below).
  • Real customers with Developer Mode on. Apple ties the test payload to the device setting, so an iOS developer who bought your app from the App Store could get it too. The AdServices check alone can't separate them from pirated copies.
  • Copies installed without Developer Mode. An install method that doesn't need Developer Mode might get a normal answer. Treat the count as a floor.
  • Failed calls. The token exchange runs once and needs the network. If it fails, you get no verdict for that user, which is different from "not pirated".

What pirated users do to your numbers

Pirated users are real people using your app. They open it, run sessions and fire events, so they count everywhere an ordinary user counts, and in my data they never paid.

  • Active users and usage. In the last 30 days pirated copies produced 10.7% of LuminaClean's events and 17.3% of BookBinge's. Daily and weekly actives, session counts and top screens all include them.
  • Every per-user conversion rate. Pirated users sit in the denominator and never in the numerator, so the measured rate is the real rate times the share of real users. With 10% pirated users, a real 5.0% conversion reads as 4.5%. At BookBinge's 41% the gap is far larger.
  • Paywall conversion, depending on the crack. LuminaClean's cracked copy never shows the paywall (0.0% of 357 views), so paywall-to-payer is unaffected there. BookBinge's does (17.5% of 63 views), so its unfiltered paywall conversion looks 17.5% worse than it is.
  • Funnels. Each step with a per-user denominator mixes two populations. In a LuminaClean funnel that ends at the paywall, part of the drop at the last step is pirated users who never see a paywall, not real users leaving.
  • Month-over-month comparisons. LuminaClean's pirated share went from 22.6% in July to 10.1% in August and 18.6% in September. Any unfiltered per-user rate moves with that share.
  • Downloads versus new users. A pirated install isn't an App Store download, so it appears in your SDK's new users and not in App Store Connect's downloads. If the two drift apart, pirated copies are one cause to check. More on what App Store Connect counts in App Store Connect analytics in 2026.

This is the same class of problem as internal traffic: a small group that behaves differently from customers and quietly bends every average. If your analytics and RevenueCat already disagree for other reasons, the list of usual causes is in why RevenueCat and your analytics numbers don't match.

What you can do about it

Filter them out of your analytics

This is what I do. The flag lives on the user, every metric excludes flagged users by default, and one switch shows them again. I keep them visible as their own row in the install-source table, so a change in the share shows up without a separate report. Filtering costs nothing, can't hurt a paying customer, and fixes the numbers whether or not you ever block anyone.

Block them, and what that would involve

I don't block pirated copies today. If you want to, this is the work involved:

  1. A signal you trust. The AdServices test payload also fires for developers with Developer Mode on, so it's a poor reason to lock someone out. A failed App Store signature (below) is a better one. Never block on "couldn't check": a phone that is offline or signed out of the App Store gives you no answer, and that isn't evidence of anything.
  2. A remote switch. Put the block behind a server-side flag that starts off, so a false positive can be undone in minutes instead of a review cycle.
  3. An expectation that it gets patched. Whoever removed LuminaClean's paywall from the binary can remove a check too.
  4. No known upside in revenue. None of the 162 and 51 pirated users paid. I have no data on whether any of them would buy if blocked, so I can't claim blocking earns anything.

A second check: the App Store's signature on the install

StoreKit 2 has a direct signal. Apple describes AppTransaction as the customer's purchase of the app, signed by the App Store, and its guide to choosing a receipt validation technique points to AppTransaction for validating the app download. A copy that didn't come from the App Store can't produce a verified one.

import StoreKit

func installVerdict() async -> String {
    do {
        switch try await AppTransaction.shared {
        case .verified(let tx):
            // production = App Store; sandbox = TestFlight or a development build; xcode = StoreKit testing
            return tx.environment == .production ? "app_store" : "test_build"
        case .unverified:
            return "unverified"   // the App Store signature didn't check out
        }
    } catch {
        return "unavailable"      // offline or not signed in: unknown, not pirated
    }
}

Apple's documentation for AppTransaction.shared notes that it throws when the transaction isn't available or the user isn't signed in to the App Store, and that it may need a network connection. That's why the catch branch has to mean "unknown".

LuminaClean 3.3 ships this check. It reports the verdict once per launch and blocks nothing. There are no results from it yet, so this post doesn't quote any. The two checks fail in different ways (one depends on Developer Mode, the other on a signature and a network call), so once both have data they can be compared user by user.

Where FolioKit fits: FolioKit flags pirated copies on the user record and leaves them out of every metric by default, together with your internal test devices; one switch shows them. The Growth tab's install integrity section flags pirated or re-signed copies. Dashboard guide.

FAQ

How can I tell if someone is using a pirated copy of my iOS app?

Two checks work from inside the app: Apple's AdServices attribution API returns a fixed test payload (IDs of 1234567890) on devices in Developer Mode, and StoreKit's AppTransaction can't be verified for a copy that didn't come from the App Store. Mark your own test devices first.

Do TestFlight and Xcode builds get Apple's AdServices test payload?

It depends on the device, not the build. Apple returns the test payload when Developer Mode is on, which it is on every phone you run Xcode builds on, so builds on those phones can get it, TestFlight included. TestFlight testers without Developer Mode get normal answers. Flag your own devices as internal.

Do users of pirated app copies ever pay?

Not in my apps: 0 of 162 in LuminaClean and 0 of 51 in BookBinge between mid-July and September 23, 2026.

How do pirated copies affect my conversion rate?

Any per-user rate is diluted by the pirated share. At 10% pirated users, a real 5.0% conversion reads as 4.5%. Paywall conversion is affected only if the cracked copy still shows your paywall.

Should I block pirated copies of my app?

Only on a signal you trust (a failed App Store signature, never a failed network call), behind a remote switch, and expecting the check to be patched out. Filtering them out of your analytics comes first and fixes the numbers either way.

Sources

FolioKit is analytics for indie iOS developers: App Store downloads and proceeds, in-app behavior and RevenueCat revenue on one user record, with pirated copies and test devices left out by default.

Start free