Skip to main content

Scheduled Data Exports

Export Transactions and Virtual Currency data to cloud storage on a schedule, with selectable columns per feed

AIAsk AIChatGPTClaude
πŸ‘Pro integration

Scheduled Data Exports are available to all users signed up after September '23, the legacy Grow and Pro plans, and Enterprise plans. If you're on a legacy Free or Starter plan and want to access this integration, migrate to our new pricing via your billing settings.

RevenueCat delivers your apps' data to a cloud storage provider on a schedule you control. Each Scheduled Data Export integration is made up of one or more feeds β€” datasets, each with its own column catalog. You choose which feeds to enable, which columns to include, what delivery cadence to run on, and whether to receive CSV or Parquet files. See Feeds for the datasets you can export today.

Setup instructions​

Feeds​

A feed is a dataset that RevenueCat exports on a schedule. Each feed has its own column catalog in the dashboard and lands in its own location in your storage destination.

The feeds available today are:

FeedDescriptionOutput path
TransactionsPer-transaction subscriber and revenue data.<base>/<date>/transactions_<timestamp>.<ext>
Virtual CurrencyPer-row virtual currency ledger: balance adjustments with running balance.<base>/virtual_currency/<date>/virtual_currency_<timestamp>.<ext>

<base> is the optional path prefix you configure on the integration (empty by default). Files are organized into dated subfolders such as 2023-01-01/. Each feed writes to its own subfolder so feeds never collide in the same destination β€” the exception is Transactions, which writes directly under <base> to keep existing integrations' paths stable. Parquet and multi-file deliveries use folder-based names instead of the single-file _<timestamp> suffix.

Enable feeds from the integration settings in the dashboard. You can enable any combination of the available feeds on a single integration.

πŸ“˜Feeds share one storage connection

All feeds on an integration use the same storage connection. You configure the bucket and credentials once, and every feed you enable delivers to that same bucket β€” what changes per feed is only the path within the bucket (see the output paths above). Enabling an additional feed doesn't create a second connection or point to a different bucket; its files simply arrive under their own path alongside your other feeds.

Selecting columns​

Each feed has a column catalog that lists every available column for that feed. You choose which columns belong in that feed's export, save, and future deliveries reflect that selection β€” past exports aren't rewritten. The default selection covers the most commonly used fields; you can expand or trim it at any time.

For step-by-step instructions, see Selecting columns for an export.

Export frequency​

By default, exports run once per day. How much data each delivery contains depends on the feed:

  • Incremental feeds can send only new and updated rows when Receive new and updated transactions only is enabled, which reduces the volume you process per delivery. On Enterprise plans, these feeds can be delivered every 4, 6, 8, 12, or 24 hours. For any cadence outside those options, contact your Customer Success Manager or visit our pricing page.
  • Full-snapshot feeds re-export the entire dataset on each run and don't support incremental delivery.

Each feed's delivery behavior is noted in its section under Available feeds.

The date and time set in Next export start time is when the next export should start being generated β€” not when it will be delivered.

Output format​

Scheduled Data Exports can deliver CSV or Parquet files. You can change the format from the integration settings in the dashboard.

CSV exports are gzip compressed, comma-delimited, and by default produced as a single file per delivery. For CSV exports larger than approximately 1 GB, enable Split export into multiple files to break each delivery into smaller files.

Parquet exports use Snappy compression and always produce multiple files per delivery; RevenueCat does not merge them.

Available feeds​

The tables below list every column available for each feed. New integrations start with a default selection of the most commonly used columns; you can adjust each feed's selection from the column catalog in the dashboard. For step-by-step instructions, see Selecting columns for an export.

Transactions​

Per-transaction subscriber and revenue data. Delivered to <base>/<date>/transactions_<timestamp>.<ext>. This is an incremental feed: with Receive new and updated transactions only enabled, each delivery includes just the rows that changed since the last one.

All dates and times are provided in UTC.

HeaderDescriptionTypeExample valueCan be null
rc_original_app_user_idCan be used as a unique user identifier to find all of a user's transactions.string$RCAnonymousID:87c6049c58069238dce29853916d624c
rc_last_seen_app_user_id_aliasCan be used together with rc_original_app_user_id to match transactions with user identifiers in your systems.string$RCAnonymousID:87c6049c58069238dce29853916d624c
countryStore country of a transaction when known, or an IP-based estimate of a subscriber's country when not known.stringGBβœ…
country_sourcefrom_sdk when the store country of a transaction is known, or estimated when country is sourced from an IP-based estimate.stringfrom_sdkβœ…
product_identifierThe product identifier that was purchased.stringrc_subscription_monthly
product_display_nameThe display name of the product identifier if one has been setstringMonthly $9.99βœ…
product_durationThe standard duration of the product if one is known by RevenueCat. May be null if RevenueCat does not know the authoritative duration.

product_duration does not represent the trial or introductory period length of a transaction, it only represents the standard duration of the product that's been subscribed to.
stringP1Mβœ…
start_timePurchase time of transaction.datetime2023-01-01 08:27:06
end_timeExpected expiration time of subscription. Null when is_auto_renewable = false
For Google Play, end_time can be before start_time to indicate an invalid transaction (e.g. billing issue).
datetime2023-02-01 08:27:06βœ…
grace_period_end_timeExpiration time of a grace period (if applicable) for a subscription. Will remain set while a subscription is in its grace period, or if it exited its grace period without renewing. Null when a subscription is not in a grace period or expiration was not due to a grace period.datetime2023-02-17 08:27:06βœ…
effective_end_timeSingle reference point of a subscriber’s expiration and entitlement revocation; inclusive of each store’s logic for refunds, grace periods, etc.datetime2023-02-17 08:27:06βœ…
storeThe source of the transaction. Can be app_store, play_store, stripe, or promotional.stringplay_store
is_auto_renewabletrue for auto-renewable subscriptions, false otherwise.booleantrue
is_trial_periodtrue if the transaction was a trial.booleanfalse
is_in_intro_offer_periodtrue if the transaction is in an introductory offer period.booleanfalse
is_sandboxtrue for transactions made in a sandbox environment.booleanfalse
price_in_usdThe revenue (converted to USD) generated from the transaction after accounting for full and partial refunds. Can be null if product prices haven't been collected from the user's device.float0βœ…
purchase_price_in_usdThe gross revenue (converted to USD) generated from the transaction. Remains set for refunded transactions. Can be null if product prices haven't been collected from the user's device.float9.99βœ…
takehome_percentage[DEPRECATED] The estimated percentage of the transaction price that will be paid out to developers after commissions, but before VAT and DST taxes are taken into account. (will be either 0.7 or 0.85)

We recommend using tax_percentage and commission_percentage to calculate proceeds instead. Learn more here.
float0.7
tax_percentageThe portion of a transaction’s price that will be deducted by the store for taxes. VAT & Digital Services Taxes may be withheld by stores depending on the store and country. To learn more about how RevenueCat estimates taxes, click here.float0.1442
commission_percentageThe portion of a transaction's price that will be deducted by the store for commission. In stores where taxes are deducted before commission, this value will not equal the published commission from a store, because that commission is calculated on the post-tax revenue.float0.15
store_transaction_idorderId or transaction_identifier.string123456789012345
original_store_transaction_idorderId of first purchase or original_transaction_id. Can be used to find all related transactions for a single subscription.string011223344556677
refunded_atWhen a refund was detected, null if none was detected. Is not set in the case of upgraded transactions for which the App Store issues a partial refund.datetime2023-02-20 05:47:55βœ…
unsubscribe_detected_atWhen we detected an unsubscribe (opt-out of auto renew).datetime2023-02-16 14:17:10βœ…
billing_issues_detected_atWhen we detected billing issues, null if none was detected.datetime2023-02-01 08:27:15βœ…
purchased_currencyThe currency that was used for the transaction.stringGBPβœ…
price_in_purchased_currencyThe revenue (in the purchased currency) generated from the transaction after accounting for full and partial refunds. Can be null if product prices haven't been collected from the user's device.float0βœ…
purchase_price_in_purchased_currencyThe gross revenue (in the purchased currency) generated from the transaction. Remains set for refunded transactions. Can be null if product prices haven't been collected from the user's device.float3.99βœ…
entitlement_identifiersAn array of entitlements that the transaction unlocked or null if it didn't unlock any entitlements.string array["membership", "full_access"]βœ…
renewal_numberAlways starts at 1. Trial conversions are counted as renewals. is_trial_conversion is used to signify whether a transaction was a trial conversion.integer2
is_trial_conversionIf true, this transaction is a trial conversion.booleantrue
presented_offeringThe offering presented to users.stringDefault Offeringβœ…
ownership_typeWill be PURCHASED when a recorded transaction results from the subscriber’s direct purchase of it, or FAMILY_SHARED when a recorded transaction results from the subscriber having received it through Family Sharing.

NOTE: The FAMILY_SHARED designation is only supported on App Store transactions.
stringPURCHASEDβœ…
reserved_subscriber_attributesThe reserved attributes set for the Customer (subscriber). Keys begin with $.string JSON{"$ip": {"value": "203.78.120.117", "updated_at_ms": 1672549200}, "$gpsAdId": {"value": "80480bdc-06e0-11ee-be56-0242ac120002", "updated_at_ms": 1672549200}, "$androidId": {"value": "12345a9876b4c123", "updated_at_ms": 1673097132390}}βœ…
custom_subscriber_attributesThe custom attributes set for the Customer (subscriber).string JSON{"feature_setting": {"value": "1", "updated_at_ms": 1672549200}, "survey_response": {"value": "2", "updated_at_ms": 1599112814785}}βœ…
platformLast seen platform of the subscriber.stringandroidβœ…
updated_atThe last time an attribute of the transaction was modified.datetime2023-02-20 05:47:55
offerThe offer that was used for a transaction (if applicable).stringblack_friday_discountβœ…
offer_typeThe type of offer that was used for a transaction (if applicable).stringoffer_codeβœ…
first_seen_timeThe time the customer was first seen by RevenueCat.datetime2023-01-01 03:00:00
auto_resume_timeThe time when a Play Store subscription would resume after being paused.datetime2023-03-20 03:00:00βœ…
enrolled_experiments_by_layerObject containing experiment enrollment information for the subscriber, keyed by layer. Replaces the deprecated experiment_id and experiment_variant columns.string JSON{"layer_name": [{"experiment_id": "prexpaaaaaaaaaa", "variant": "a", "enrolled_at": "1970-01-01 00:00:00"}]}βœ…

Virtual Currency​

Per-row virtual currency ledger data. Delivered to <base>/virtual_currency/<date>/virtual_currency_<timestamp>.<ext>. This is a full-snapshot feed: each run re-exports the entire ledger and it doesn't support incremental delivery. To set up virtual currencies in your project before exporting them, see Virtual Currency.

The Virtual Currency feed exports one row per balance adjustment in your virtual currency ledger. Each row records a credit or debit, the running balance after that adjustment, and β€” when the adjustment originated from a store purchase β€” the associated purchase fields.

Purchase-related columns (store, is_sandbox, store_transaction_id, product_identifier, and price fields) are null on non-purchase rows. Refund columns (refund_amount_usd, refund_amount_in_purchased_currency, refund_type) are populated only on REFUND_REMOVAL rows with a positive refund delta.

All dates and times are provided in UTC.

HeaderDescriptionTypeExample valueCan be null
rc_original_app_user_idThe original app user ID for the subscriber. Can be used as a unique user identifier to find all of a user's virtual currency events.string$RCAnonymousID:87c6049c58069238dce29853916d624cβœ…
rc_last_seen_app_user_id_aliasThe last seen alias for the app user ID. Can be used together with rc_original_app_user_id to match events with user identifiers in your systems.string$RCAnonymousID:87c6049c58069238dce29853916d624cβœ…
transaction_typeVirtual currency event type. One of PURCHASE, DEDUCTION, GRANT, REFUND_REMOVAL, expiration, or other.stringPURCHASEβœ…
currency_codeCode of the virtual currency.stringcoinsβœ…
adjustment_amountSigned amount of this balance adjustment. Positive values are credits; negative values are debits.integer100βœ…
updated_balanceRunning balance for this balance ID after this adjustment, floored at 0.integer250βœ…
event_timestampWhen the underlying virtual currency transaction occurred.datetime2023-01-01 08:27:06βœ…
updated_atWhen this ledger row was last updated in RevenueCat.datetime2023-02-20 05:47:55βœ…
idempotency_keyIdempotency key of the underlying virtual currency transaction. Use this to deduplicate rows if you ingest the same delivery more than once.stringvc_tx_abc123βœ…
storeStore the originating purchase came from (app_store, play_store, etc.). null for non-purchase rows.stringapp_storeβœ…
is_sandboxWhether the originating purchase was made in a sandbox environment. null for non-purchase rows.booleanfalseβœ…
store_transaction_idIdentifier the store assigned to the originating purchase.string123456789012345βœ…
product_identifierIdentifier of the product purchased.stringcoins_100_packβœ…
price_in_usdPrice of the purchase in USD, after any refunds.float4.99βœ…
purchase_price_in_usdCatalog price of the purchase in USD, before discounts and refunds.float4.99βœ…
purchased_currencyCurrency the purchase was made in.stringUSDβœ…
price_in_purchased_currencyPrice of the purchase in its purchased currency, after any refunds.float4.99βœ…
purchase_price_in_purchased_currencyCatalog price of the purchase in its purchased currency, before discounts and refunds.float4.99βœ…
refund_amount_usdRefunded amount in USD. Populated only on REFUND_REMOVAL rows with a positive refund delta.float4.99βœ…
refund_amount_in_purchased_currencyRefunded amount in the purchased currency. Populated only on REFUND_REMOVAL rows with a positive refund delta.float4.99βœ…
refund_typeFULL or PARTIAL. Populated only on REFUND_REMOVAL rows with a positive refund delta.stringFULLβœ…

A note on transaction data​

The following guidance applies to the Transactions feed.

All transaction data is based on the store receipts that RevenueCat has received. Receipts often have inconsistencies and quirks that you may need to account for. For example:

  • The expiration date of a purchase can be before the purchase date. This is Google's way of invalidating a transaction, for example when Google is unable to bill a user some time after a subscription renews. This doesn't occur on iOS.
  • If you migrated to RevenueCat, Google subscriptions that were expired for more than 60 days before being migrated will not have transaction histories in export files.
  • Apple and Google do not always provide the transaction price directly, so we rely on historical data and store APIs. This may result in inaccuracies if receipts were imported, or if a product price was increased before your App Store Connect API Key was added.
  • Renewal numbers start at 1, even for trials. Trial conversions increase the renewal number.
  • Data is pulled from a snapshot of the current receipt state. This means the same transaction can differ from one delivery to another if something changed (for example, due to a refund or billing issue). Recompute metrics for past time periods periodically to account for these changes, and use the updated_at field to detect whether a transaction changed since a prior export.
  • Data is current as of when the export begins generating. Changes that occur between the start of generation and delivery aren't reflected in that export.

We try to normalize or at least annotate these quirks as much as possible, but by and large we consider receipts the source of truth, so any inconsistencies in the transaction data can always be traced back to the receipt.

Handling updated transactions correctly​

These recommendations apply to the Transactions feed.

We strongly recommend keeping Receive new and updated transactions only enabled to significantly reduce the amount of data you need to process in each daily export.

The Receive new and updated transactions only option in the integration settings

However, handling transaction updates can be tricky, so consider these tips to make it easier:

  1. For most stores, store_transaction_id will be unique for each transaction, but for Stripe it is not; so for best results we recommend treating every unique set of [store_transaction_id + renewal_number] as a unique transaction.
  2. Instead of overwriting prior transaction states when receiving an updated transaction, consider adding them as new rows to your output table and setting a property like is_latest to ensure you're never double-counting different versions of the same transaction. Or, you could set an ingested_time property to order the transactions by the most recent version you received from RevenueCat.
  3. When in doubt, use updated_at (provided by RevenueCat in your export) as a reference point to determine the latest version of a transaction if you have multiple prior versions and can't otherwise confidently determine which one is latest.

Sample queries for RevenueCat measures​

The following sample queries use the Transactions feed. They are written in PostgreSQL and serve as starting points for reproducing common RevenueCat measures.

-- Active Subscriptions as of a specified date

SELECT
COUNT(*)
FROM
[revenuecat_data_table]
WHERE date(effective_end_time) > [targeted_date]
AND date(start_time) <= [targeted_date]
AND is_trial_period = 'false'
AND DATE_DIFF('s', start_time, end_time)::float > 0
AND ownership_type != 'FAMILY_SHARED'
AND store != 'promotional'
AND is_sandbox != 'true'

-- The RevenueCat Active Subscriptions chart excludes trials,
-- promotional transactions, and transactions resulting from family sharing
-- since they do not reflect auto-renewing future payments.

Sample queries for customized measures​

These examples also use the Transactions feed. Scheduled Data Exports are a powerful way to add your own customizations on top of the core measures provided by RevenueCat. Check out the following sample queries (written in PostgreSQL) for some ideas.

-- How many Active Subscriptions do I have with a given custom attribute value?

SELECT
COUNT(*)
FROM
[revenuecat_data_table] rc

WHERE date(effective_end_time) > [targeted_date]
AND date(start_time) <= [targeted_date]
AND is_trial_period = 'false'
AND DATE_DIFF('s', start_time, end_time)::float > 0
AND ownership_type != 'FAMILY_SHARED'
AND store != 'promotional'
AND is_sandbox != 'true'
AND json_extract_path_text(custom_subscriber_attributes, '[custom_attribute_key].value') = [custom_attribute_value]
Was this page helpful?