A small CLI that removes test / TestFlight / simulator customers from your RevenueCat projects without ever touching a real paying user. Sandbox and TestFlight builds create anonymous RevenueCat customers that pollute your New-Customer charts and cohort analytics; this tool finds and deletes exactly those, and leaves everyone else alone.
A customer is test only if you can prove they are not a real App Store
buyer. rc-clean pairs each RevenueCat customer's last-seen app version against
Apple's App Store submissions and TestFlight build inventory:
- The app has never been released → any customer must be TestFlight/sim.
- The customer's last-seen version was never
READY_FOR_SALE— including TF-only marketing versions (from ASC builds) and versions still in review / prepare → test. - Sandbox-only purchasers (no real App Store purchase) → test.
- Non-purchasers first seen before the earliest public release → test.
- Anyone with a real (non-sandbox) purchase is never deleted, full stop.
Because it re-checks Apple's live release states and TF builds on every run, it
self-corrects as builds get approved. It uses Apple's public first-release date
when available, then App Store Connect's releaseDate, and finally the
earliest live build upload when the date fields are empty.
git clone https://github.com/jackwallner/rc-clean.git
cd rc-clean
pip install pyjwt requests1. Your app map — copy the example and fill in your projects:
mkdir -p ~/.rc-clean
cp apps.example.json ~/.rc-clean/apps.json{
"com.you.app": {
"rc": "<revenuecat-project-id>",
"asc": "<app-store-connect-app-id>",
"name": "Your App",
"allow_versions": []
}
}The RevenueCat project id is the short hash in your dashboard URL; the ASC app
id is the numeric id in your App Store Connect app URL. rc-clean auto-detects
which app you mean by walking up from the current directory to the nearest
project.yml / Xcode project and matching its bundle id — so just run it from
inside an app repo.
allow_versions is an optional list of marketing versions to keep regardless
of App Store or TestFlight status. Values are canonicalized, so 1.3.0 and
1.3 match the same version. Use it only for deliberate per-app exceptions.
2. App Store Connect API key — a shell-style file at
~/.rc-clean/asc_credentials exporting an App Store Connect API key with app
access:
export ASC_API_KEY_ID="XXXXXXXXXX"
export ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export ASC_KEY_PATH="$HOME/.appstoreconnect/AuthKey_XXXXXXXXXX.p8"3. RevenueCat auth — the tool uses a RevenueCat dashboard session token (the public REST API can't list or delete customers). Log in at app.revenuecat.com, open DevTools, and run:
fetch('/v1/developers/login/refresh-token', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include'
}).then(r => r.json()).then(d => copy(d.authentication_token))Then seed it (stored at ~/.revenuecat/auth_token, chmod 600, and
self-refreshing thereafter):
./rc-clean-test-users --reseed <paste-token>cd ~/my-app-repo
./rc-clean-test-users # dry run: list what WOULD be deleted
./rc-clean-test-users --delete # actually delete
./rc-clean-test-users --all-apps # every app in your config (dry run unless --delete)Dry run is the default; nothing is deleted until you pass --delete.
=== My App (RC a1b2c3d4) bundle com.you.app ===
released versions: NONE — app not public
scanning 12 customers...
real (kept): 0 purchasers (kept): 0 TEST: 12
test by version: (no detail)×5, 1.0.0×7
(dry run — pass --delete to remove these 12)
| What | Default | Override |
|---|---|---|
| App map | ~/.rc-clean/apps.json |
$RC_CLEAN_APPS |
| ASC creds | ~/.rc-clean/asc_credentials |
$RC_CLEAN_ASC_CREDS |
| RC token | ~/.revenuecat/auth_token |
$RC_CLEAN_TOKEN |
- RevenueCat deletion is asynchronous; a deleted customer 404s within minutes but may still appear in the enumeration index briefly. Re-running is safe and idempotent.
- Ghost/anonymous rows whose detail endpoint 404s are reported as stale and skipped. They are already absent from the customer detail endpoint.
MIT — see LICENSE.