Rovenue SDK
Resources

Migrating from RevenueCat

Concept mapping and step-by-step history import from RevenueCat to Rovenue.

Rovenue covers the same subscription-management use cases as RevenueCat but runs on your own infrastructure. This page maps familiar RevenueCat concepts to their Rovenue equivalents so you can orient quickly, then walks through importing your existing subscriber and transaction history with the built-in data import tool.

Concept mapping

RevenueCatRovenue
Purchases.configure(apiKey)Rovenue.configure({ apiKey }) — baseUrl is optional (self-hosting only)
getCustomerInfo().entitlementsentitlement(id) / entitlementsAll()
logIn(appUserID)identify(appUserId) — server-side bind + auto-transfer of prior installs
Purchases.getOfferings()Rovenue.getOfferings()
SDK-driven purchase(package)Rovenue.purchase(pkg) — same pattern; getOfferings() then purchase(pkg)
addCustomerInfoUpdateListeneraddChangeListener (RN) / changes AsyncStream (Swift) / SharedFlow (Kotlin)

Purchase flow

The purchase flow in Rovenue mirrors RevenueCat's: call getOfferings() to fetch your configured products, then pass the chosen package to purchase(pkg). The SDK triggers the native store sheet (StoreKit 2 on iOS, Play Billing on Android), validates the receipt server-side, and resolves with updated entitlements and credit balance — no separate receipt-posting step required.

const offerings = await Rovenue.getOfferings();
const pkg = offerings.current?.packages[0];

if (pkg) {
  try {
    const result = await Rovenue.purchase(pkg);
    if (result.isDeferred) { /* Ask-to-Buy / deferred — not an error, wait for it to resolve */ }
    // result.entitlements / result.virtualCurrencies are already up to date
  } catch (e) {
    if (e instanceof RovenueError && e.kind === 'PurchaseCanceled') { /* user dismissed — ignore */ }
    else throw e;
  }
}

await Rovenue.restorePurchases();

Key differences from RevenueCat

  • Self-hosted. Rovenue runs on your own infrastructure — no revenue share, no third-party data processing. You own the database and control retention.
  • Virtual currencies built-in. Rovenue ships a first-class multi-currency ledger for consumable products, loyalty systems, and metered billing. Balances are read client-side; spending is a server-side operation via POST /v1/virtual-currencies/:appUserId/:code/transactions.
  • Identity model. identify(appUserId) performs a server-side bind and automatically transfers any prior install's purchases and credits to the current device (similar to RevenueCat's "transfer to new ID" behaviour). Call logOut() on sign-out to mint a fresh rovenueId. Bulk or programmatic merges are available via the secret-key backend API.
  • Open source (AGPLv3). You can read, audit, and fork the full stack.

Importing your history

Rovenue's data import tool is a column-mapping engine with vendor presets, not a hand-written RevenueCat parser — it reads a CSV, maps its columns onto a fixed set of canonical fields (subscriber id, store, product identifier, purchase date, and so on), and writes the result as history. If a column layout ever drifts, the import falls back to the generic mapper below instead of guessing.

A full RevenueCat migration is two uploads, not one: the Transactions export (your whole history), then — for Android only — the supplemental purchase-token file, which attaches Google Play purchase tokens to the Android purchases the first import already wrote. Both are covered below, in order.

Export from RevenueCat

From the RevenueCat dashboard, request a Transactions export. This is a CSV with one row per transaction (initial purchase, renewal, refund, and so on) — the closest RevenueCat gets to a full history dump.

The standard Transactions export does not include google_purchase_token. Android rows from it import as history only — they still create a subscriber, purchase and revenue-event history, so your dashboards and LTV numbers are complete — but they cannot be re-verified against the Play Developer API, so they carry no live entitlement grant from this import. The dry run quantifies exactly how many rows this affects before you commit, and the purchase-token second pass further down recovers them.

Every other row-level field the exporter needs — subscriber id, store, product identifier, purchase/expiry dates, trial and intro flags, USD price, entitlement identifiers — is present in the standard export and is picked up automatically once the file is uploaded, because its header matches the RevenueCat Transactions preset. store_transaction_id specifically is included on the strength of RevenueCat's own documented guidance that it's the transaction row's unique key, not confirmed column-by-column against a real export at the time this guide was written — if your export happens to lack it, preset detection simply won't match and you'll map by hand in the generic-mapper step below.

Upload the export

Upload the Transactions export to the import tool. Its header carries the RevenueCat Transactions preset's anchor columns (rc_original_app_user_id, store_transaction_id), so the tool proposes that preset's mapping automatically — review it, adjust anything you've customized, and continue to the dry run and commit steps below. This single pass imports every row — iOS, Android, Stripe — as history, and grants live access for every row that already carries a usable store anchor (everything except Android rows, covered next).

Android live entitlements: the purchase-token second pass

Ask RevenueCat support for the supplemental purchase-token CSV. It has three columns — user_id, google_purchase_token, google_product_id — one row per Android subscription. Upload it to the import tool exactly like the Transactions export; its header matches the revenuecat_google_token preset, so the tool recognises it, proposes the three-column mapping, and marks the job as a Google purchase-token second pass.

This pass creates nothing. It attaches each token to the Android purchase rows your history import already wrote, which is what lets Rovenue re-verify those subscriptions against the Play Developer API and grant live access. Run it after the history import has completed, or it will find nothing to attach to.

Run the history import first. A token whose subscriber or product has no imported Play purchase is reported as No matching Play purchase and skipped. That is also the signal to check that the product's Google store id is in your Rovenue catalog — the pass matches on the catalog product, not on the raw string.

Unlike the history import, this pass reads the whole file into memory before it resolves anything — it has to, because "this file gives one subscriber and product two different tokens" is a property of the file, not of any single row. Files above 500,000 rows are therefore refused outright rather than partly enriched, with a message saying so; split one that large and upload each part as its own import. The pass is idempotent, so overlapping parts are safe.

The dry run classifies every row and writes nothing, exactly as it does for a history import. Its buckets are:

OutcomeMeaning
Token appliedThe token was matched to one subscription and will be written to every row of it.
Already had this tokenNothing to do — re-running the same file is a no-op.
Renewals not linked — needs an opt-inSee below.
Ambiguous — skippedReported, never guessed at. See below.
Conflicting token already stored — skippedA purchase row already stores a different token. Never overwritten.
No matching Play purchaseNo imported Play purchase for that subscriber and product.
Invalid rowThe row is missing one of the three required values.

What it refuses to guess

A subscriber can hold more than one subscription to the same product — they lapsed and resubscribed. (subscriber, product) is therefore not a unique key on purchases, and picking one subscription silently would be a guess dressed up as a lookup. So when a row's subscriber and product span more than one subscription chain, the pass reports it as ambiguous and skips it rather than choosing. Stamping one subscription's token onto another's renewals would make verification confidently re-check the wrong subscription — worse than leaving the rows as history.

The same applies to an ambiguity your file asserts: if it supplies two different tokens for one subscriber and product, nothing can resolve that and no setting overrides it.

enrichUngroupedChains — the opt-in you will probably need

The Transactions export has no original-transaction column. Rovenue's importer therefore cannot tell that a subscriber's four Android renewals belong to one subscription, and records each as its own one-row chain. Under the strict rule above, that is "four chains" and every one of them is refused — so on a stock RevenueCat export the second pass can end up enriching nothing at all.

That is what the Link unlinked renewals checkbox (enrichUngroupedChains) is for. Turn it on and the pass treats a subscriber's unlinked renewals of one product as a single subscription. The dry-run summary names it, with a count, whenever rows land in that bucket — turn it on, save, and re-run the dry run to see what it changes before committing.

It is deliberately narrow:

  • It applies only when the history export expressed no subscription chains at all. If even one imported row carries a real original-transaction id, the export did express chains, and more than one of them is a genuine resubscribe — still refused.
  • It never resolves two different tokens for one subscriber and product.
  • It relaxes grouping, never overwriting. A stored token that disagrees with your file is never replaced under any setting.

It is off by default because the strict rule is the safe direction.

If you skip this pass entirely

Nothing breaks. Android rows stay as complete purchase/revenue history and LTV; only the live entitlement grant is missing. Once you cut a user's app over to the Rovenue SDK (last step below), their next restorePurchases() call — or any live purchase/renewal webhook — re-verifies directly against the Play Developer API and grants access from then on. The second pass is what covers the users whose devices never open the app again.

Map anything the presets don't cover

If your export has been customized (extra columns, a renamed subscriber attribute) or a column doesn't match either preset by name, the import tool falls back to the generic mapper: you assign each source column to one of the canonical fields yourself. subscriberExternalId, store, productIdentifier, and purchaseDate are required; everything else is optional and simply isn't imported if left unmapped. Preset detection is anchor-gated — a file with no vendor-identifying column detects nothing, which is expected, not an error, and just means you map by hand.

The store column only recognizes exactly these values (matching is case-insensitive, but nothing else is guessed): app_store, play_store, stripe, promotional. A column that instead reads App Store, ios, android, or google will fail every row in the file with an unrecognized-value error — this is the single most common way a hand-mapped file fails completely. Recode the column (or transform it before export) to one of the four accepted values first.

Dry run

Every import runs a dry run before it can commit — there's no way to skip it. Starting one queues an asynchronous job (the dashboard polls for completion); on a large export this can take a while, exactly because it reads the whole file. The dry run reports, per row, whether it will create a new purchase, update an existing one, or be skipped (unresolved product, sandbox, no Android token, invalid data) — including the count of rows that will import as history-only with no live entitlement — the rows the purchase-token second pass above exists to recover. Review those numbers before committing.

Verify and commit

Committing runs two phases. Phase A writes every row the file carries as history — subscribers, purchases, revenue events — without contacting Apple, Google, or Stripe. Phase B then re-validates every row that carries a real store anchor against the store itself and sets its live status. A row the store no longer recognises is left alone as history, never deleted — this is a deliberate difference from how some other importers handle a stale transaction, and it means your data is never silently discarded because a transaction predates what the store API still remembers.

Large imports can outrun a single verification pass — store APIs are rate-limited, and Android in particular is a documented quota hazard. If Phase B runs out of budget partway through, the job ends in VERIFICATION_INCOMPLETE rather than failing outright; every anchor already resolved is remembered, so resuming the job continues from where it left off instead of re-verifying from scratch. A job the tool is still actively verifying shows as VERIFYING.

Cut the SDK over

Once you're satisfied with the import, point your app at Rovenue: install the SDK for your platform, call configure(), and call identify(appUserId) with the same id your export used as rc_original_app_user_id / subscriberExternalId so the SDK resolves to the subscriber your import created. restorePurchases() re-validates any purchase the store still knows about, which is a useful sanity check immediately after cutover.

What doesn't survive the move

Rovenue's import is a best-effort history reconstruction, not a full data migration, and no importer — in either direction — can promise otherwise. Specifically, the following are not carried over:

  • Full event history. The import writes purchases and revenue events from the export's transaction rows; it does not reconstruct every underlying lifecycle event (webhooks, state transitions) RevenueCat ever recorded.
  • Original historical prices where the export itself doesn't carry them — the import only ever uses the price a row actually reports; it never estimates or backfills one.
  • Full renewal chains beyond what the export lists as individual rows. If RevenueCat's export doesn't include a renewal, Rovenue has no way to know it happened.
  • Promotional entitlements with no underlying store transaction import as a MANUAL grant with a deterministic id (so re-running the import doesn't duplicate them) — but only when the export's store column marks the row promotional. A grant made outside RevenueCat's own exported data model has nothing to import from.

Uploaded export files are end-user PII and are deleted automatically after a retention window once a job finishes. A job still actively resumable (VERIFICATION_INCOMPLETE / VERIFYING) is never swept while it's in that state — the sweep would strand it incomplete forever. A FAILED or CANCELLED job, though also re-runnable from the mapping-editor step without re-uploading, IS swept on the same clock as a finished job once the window elapses; plan any mapping fix and retry within that window, or re-upload the same file as a new job afterward.

On this page