Virtual Currency
Virtual currencies are digital assets used within your app to facilitate transactions, unlock premium features, or enhance customer engagement. These currencies are typically acquired through in-app purchases, rewards, or gameplay achievements and do not have intrinsic real-world value outside the application. They can be used for purchasing virtual goods, upgrading characters, or accessing exclusive content. Common examples include tokens, coins, credits, or other units that can be replenished through purchases. You can leverage virtual currencies to monetize apps, encourage customer retention, and create a more immersive experience.
This feature is in an early stage and under active development. While what's available today is stable and ready to use, we're continuing to expand its capabilities.
We’d love to hear your feedback to help shape the roadmap.
Configuration
In RevenueCat, virtual currencies are defined at the project level. You can configure up to 100 virtual currencies per project and use them to enrich your app experience.
- Click the “Virtual Currencies” option in the “Product catalog” of your project sidebar in RevenueCat
- Select “+ New virtual currency” button in the top right corner. Enter a code and a name for your currency.
- Code: This is used across various APIs to identify the virtual currency (e.g: GLD)
- Icon (optional): Choose an icon to visually represent this currency in the dashboard
- Name: This should be a readable name for the currency that you're creating (e.g: GOLD)
- Description (optional): A description of what this currency represents or how it is used (e.g: Can be used to purchase in-app items)
- You can optionally associate products with your new currency. Every time customers purchase one of these products, the defined amount of virtual currency will be added to their balance. Click Add associated product, pick a product and fill in the amount.
You can associate as many products as you want with your virtual currency and you can also associate a product with more than one virtual currency, meaning once it's purchased, multiple types of virtual currencies are added to the customer's balance. If you have not yet configured any products, see our documentation for further instructions.
- Remember to select "SAVE" in the upper right-hand corner. Repeat this process if to create more than one currency.
Dashboard balances
Once your customer purchase the associated products they will get the defined amount of your virtual currency. You can inspect the virtual currency balances of your customer in the right side-panel of the customer page.
Usage
Prerequisites
The endpoints available for virtual currency are supported through our Developer API (2.0.0). You will need a secret key to access it. Make sure that your key at least has Read & Write permissions for Customer Purchases Configuration and Customer Configuration. See our documentation for more details on how you can access RevenueCat’s Developer API.
Rate limits
All endpoints for virtual currency under our Developer API are subject to a rate limit of 480 requests per minute. If you exceed this limit, the API will return a 429 Too Many Requests status code, indicating that the rate limit has been reached.
To avoid service interruptions, we recommend implementing retry logic. If you hit the rate limit, the API response will include a Retry-After
header, specifying the amount of time (in seconds) you need to wait before making further requests.
For more information on handling rate limits and using the Retry-After header, please refer to our API documentation.
Limitations
The maximum amount of a single virtual currency that a customer can own must be between zero and two billion (2,000,000,000). Negative balances are not supported.
Reading balances
From your backend
The virtual currency get balance Developer API endpoint allows you to retrieve a customer's current balance from your backend:
- Code
curl --location 'https://api.revenuecat.com/v2/projects/<YOUR_PROJECT_ID>/customers/<YOUR_CUSTOMER_ID>/virtual_currencies' \
--header 'Authorization: Bearer sk_***************************'
The response will include the balances for all the virtual currencies that the customer has.
- Code
{
"items": [
{
"balance": 80,
"currency_code": "GLD",
"object": "virtual_currency_balance"
},
{
"balance": 40,
"currency_code": "SLV",
"object": "virtual_currency_balance"
}
],
"next_page": null,
"object": "list",
"url": "https://api.revenuecat.com/v2/projects/<YOUR_PROJECT_ID>/customers/<YOUR_CUSTOMER_ID>/virtual_currencies"
}
From the SDK
Fetching virtual currency balances is supported in the following SDK versions:
SDK | Supported Versions |
---|---|
iOS SDK | 5.32.0+ |
Android SDK | 9.1.0+ |
React Native SDK | 9.1.0+ |
Flutter SDK | 9.1.0+ |
You can use the virtualCurrencies()
function to retrieve a customer's balance. The function returns a VirtualCurrencies
object, which includes the customer's balances along with each virtual currency's metadata.
- Swift
- Kotlin
- Java
- React Native
- Flutter
// Fetch virtual currencies
// With Async/Await
let virtualCurrencies = try? await Purchases.shared.virtualCurrencies()
// With Completion Handlers
Purchases.shared.virtualCurrencies { virtualCurrencies, error in
// TODO: Handle virtual currencies & error
}
// Get the details of a specific virtual currency
let virtualCurrency = virtualCurrencies.all[<your_virtual_currency_code>]
let balance = virtualCurrency?.balance
let name = virtualCurrency?.name
let code = virtualCurrency?.code
// Keep in mind that serverDescription may be null if no description was provided
// in the RevenueCat dashboard
let description = virtualCurrency?.serverDescription
// Iterate through all virtual currencies
for(virtualCurrencyCode, virtualCurrencyInfo) in virtualCurrencies.all {
print("\(virtualCurrencyCode): \(virtualCurrencyInfo.balance)")
}
// Fetch virtual currencies
// With coroutines
try {
val getVirtualCurrenciesResult: VirtualCurrencies = Purchases.sharedInstance.awaitGetVirtualCurrencies()
// TODO: Handle virtual currencies
} catch(error: PurchasesException) {
// TODO: Handle error
}
// With callbacks
Purchases.sharedInstance.getVirtualCurrenciesWith(
onError = { error: PurchasesError ->
// TODO: Handle error
},
onSuccess = { virtualCurrencies: VirtualCurrencies ->
// TODO: Handle virtual currencies
},
)
// Get the details of a specific virtual currency
val virtualCurrency = virtualCurrencies.all[<your_virtual_currency_code>]
val balance = virtualCurrency?.balance
val name = virtualCurrency?.name
val code = virtualCurrency?.code
// Keep in mind that serverDescription may be null if no description was provided
// in the RevenueCat dashboard
val description = virtualCurrency?.serverDescription
// Iterate through all virtual currencies
for ((virtualCurrencyCode, virtualCurrency) in virtualCurrencies.all) {
println("$virtualCurrencyCode: ${virtualCurrency.balance}")
}
// Fetch virtual currencies
Purchases.sharedInstance.getVirtualCurrencies(
new GetVirtualCurrenciesCallback() {
@Override
public void onReceived(@NonNull VirtualCurrencies virtualCurrencies) {
// TODO: Handle virtual currencies
}
@Override
public void onError(@NonNull PurchasesError error) {
// TODO: Handle error
}
}
);
// Get the details of a specific virtual currency
VirtualCurrency virtualCurrency = virtualCurrencies.getAll().get(<virtual_currency_code>);
int balance = virtualCurrency.getBalance();
String name = virtualCurrency.getName();
String code = virtualCurrency.getCode();
// Keep in mind that serverDescription may be null if no description was provided
// in the RevenueCat dashboard
String serverDescription = virtualCurrency.getServerDescription();
// Iterate through all virtual currencies
virtualCurrencies.getAll().forEach((key, virtualCurrency) -> {
System.out.println(virtualCurrency.getCode() + ": " + virtualCurrency.getBalance());
});
// Fetch virtual currencies
function _array_like_to_array(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
return arr2;
}
function _array_with_holes(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterable_to_array_limit(arr, i) {
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
if (_i == null) return;
var _arr = [];
var _n = true;
var _d = false;
var _s, _e;
try {
for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally{
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally{
if (_d) throw _e;
}
}
return _arr;
}
function _non_iterable_rest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _sliced_to_array(arr, i) {
return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
}
function _unsupported_iterable_to_array(o, minLen) {
if (!o) return;
if (typeof o === "string") return _array_like_to_array(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(n);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
}
try {
var _$virtualCurrencies = await Purchases.getVirtualCurrencies();
// TODO: Handle virtual currencies
} catch (error) {
// TODO: Handle error
}
// Get the details of a specific virtual currency
var virtualCurrency = virtualCurrencies.all['<virtual_currency_code>'];
var balance = virtualCurrency.balance;
var name = virtualCurrency.name;
var code = virtualCurrency.code;
// Keep in mind that serverDescription may be null if no description was provided
// in the RevenueCat dashboard
var serverDescription = virtualCurrency.serverDescription;
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
try {
// Iterate through all virtual currencies
for(var _iterator = Object.entries(virtualCurrencies.all)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
var _step_value = _sliced_to_array(_step.value, 2), virtualCurrencyCode = _step_value[0], virtualCurrency1 = _step_value[1];
console.log("".concat(virtualCurrency1.code, ": ").concat(virtualCurrency1.balance));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally{
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally{
if (_didIteratorError) {
throw _iteratorError;
}
}
}
// NOTE: Virtual currencies are not yet supported in Flutter web and
// are only available for Flutter on iOS and Android.
// Fetch virtual currencies
try {
final virtualCurrencies = await Purchases.getVirtualCurrencies();
// TODO: Handle virtual currencies
} catch (error) {
// TODO: Handle error
}
// Get the details of a specific virtual currency
final virtualCurrency = virtualCurrencies.all[<virtual_currency_code>];
final balance = virtualCurrency?.balance;
final name = virtualCurrency?.name;
final code = virtualCurrency?.code;
// Keep in mind that serverDescription may be null if no description was provided
// in the RevenueCat dashboard
final serverDescription = virtualCurrency?.serverDescription;
// Iterate through all virtual currencies
virtualCurrencies.all.forEach((virtualCurrencyCode, virtualCurrency) {
print('$virtualCurrencyCode: ${virtualCurrency.balance}');
});
When a customer's balance is updated from your backend, the VirtualCurrencies
object remains cached and is not automatically updated. To get the updated balance, you need to call Purchases.shared.invalidateVirtualCurrenciesCache()
and fetch the VirtualCurrencies
object again.
We also recommend calling invalidateVirtualCurrenciesCache()
after a purchase has completed successfully to ensure that the balances are up to date the next time you fetch them.
You may directly access the cached virtual currencies using the cachedVirtualCurrencies
property. This is helpful for rendering UI immediately, or for displaying virtual currencies when there is no network connection. Keep in mind that this value is cached and isn't guaranteed to be up to date.
- Swift
let cachedVirtualCurrencies: VirtualCurrencies? = Purchases.shared.cachedVirtualCurrencies
Hybrid SDK Support
Virtual currency support in the hybrid SDKs is coming soon!
Depositing or spending
You can deposit or spend currency by calling the virtual currency transactions Developer API endpoint from the backend of your app:
- Code
curl --location 'https://api.revenuecat.com/v2/projects/<YOUR_PROJECT_ID>/customers/<YOUR_CUSTOMER_ID>/virtual_currencies/transactions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer sk_***************************' \
--data '{
"adjustments": {
"GLD": -20,
"SLV": -10
}
}'
The example request will deduct 20 GLD and 10 SLV from the customer's balance. Upon successful execution, the response will contain the updated balances of the virtual currencies that were spent.
Note that sufficient balances of both currency types are required for the transaction to succeed. If not, the transaction will fail with HTTP 422 error and no virtual currency will be deducted.
- 200
- 422
{
"items": [
{
"balance": 80,
"currency_code": "GLD",
"object": "virtual_currency_balance",
},
{
"balance": 40,
"currency_code": "SLV",
"object": "virtual_currency_balance",
},
],
"next_page": null,
"object": "list",
"url": "https://api.revenuecat.com/v2/projects/<YOUR_PROJECT_ID>/customers/<YOUR_CUSTOMER_ID>/virtual_currencies"
}
{
"doc_url": "https://errors.rev.cat/unprocessable-entity-error",
"message": "Customer's balance is not enough to perform the transaction.",
"object": "error",
"param": "adjustments",
"retryable": false,
"type": "unprocessable_entity_error"
}
Multiple virtual currency types can be adjusted in a single transaction. Deductions and additions can also be combined. For example, you can execute the conversion of 50 GLD to 200 SLV with the following transaction:
- Code
{
"adjustments": {
"GLD": -50,
"SLV": 200
}
}
Events
RevenueCat provides event tracking for virtual currency transactions, allowing you to monitor and respond to balance changes in real-time through webhooks.
Virtual currency transactions appear in the Customer History timeline and trigger VIRTUAL_CURRENCY_TRANSACTION
webhook events for the entire subscription lifecycle whenever there are currency balance adjustments.
Virtual currency adjustments made through the API will appear in the customer timeline, but cannot be clicked for additional details. These adjustments are displayed for reference only and do not generate webhook events.
For more information about virtual currency events, including customer timeline events and webhook events, see our Virtual Currency Events documentation.
RevenueCat's Firebase Extension integration
If you have the Firebase Extension integration enabled, RevenueCat will include the customer's virtual currency balance in the dispatched events' payload. Balance changes due to other reasons, such as manual adjustments through our Developer API endpoints will not trigger an event.
When an event is dispatched, the Firebase Extension event payload will include a virtual_currencies
object containing the customer's total balance for each currency after the purchase. The below example shows the customer's total balance for "GLD" is 80, while "SLV" is 40.
- Code
{
"virtual_currencies": {
"GLD": {
"balance": 80
},
"SLV": {
"balance": 40
}
}
}
Best practices and security considerations
Virtual currencies is a very powerful feature that RevenueCat provides, however it needs to be used correctly to ensure high standards of security. Here are some necessary requirements in order to make sure that bad actors cannot exploit your system for their benefit or to harm other users of your app.
Virtual currency transactions should be securely initiated by a backend server
Transactions that add or remove virtual currencies to your customer balances, except for In-App Purchases, should be initiated by the backend of your Application. These requests require RevenueCat secret API keys to be authenticated, and these keys need to be securely stored and never be exposed to the public.
It’s fine if your backend provides APIs for your app to initiate virtual currency transactions, however, these APIs should not allow direct modifications of customer balances. Instead they should only support operations that do not require direct input of amounts and they should always perform the necessary validations to ensure that the customer has the rights and meets the requirements to perform the requested transaction.
See some examples of secure and unsecure backend APIs:
Do ✅ | Do not! ❌ |
---|---|
|
|
|
|
Following this will ensure that the users of your app cannot tamper / fake requests to your backend for their benefit.
Communication between your app and your backend should be encrypted and authenticated
All the requests from your app to your backend that could trigger a virtual currency transaction need to be encrypted and authenticated. Make sure you use TLS or equivalent encryption technologies. Also ensure that all the requests that can trigger a virtual currency transaction are authenticated using well proved methodology.
Here are a few options to consider:
- Password based authentication
- Two/Multifactor authentication
- Token based authentication (e.g. JWT, OAuth 2.0)
- Single sign on using widely used services (Google, Facebook, Apple etc)
- Other equivalent or stronger technologies
With this you will ensure that requests that could trigger virtual currency transactions for an account of your app can only be initiated by the actual account owner.
Tips & Hints
Ensuring exactly one execution of Virtual Currency transactions
As a common practice, you may implement retries to handle network or other errors when submitting a Virtual Currency transaction. If you want to ensure that your transaction will only be executed once, even if your request reaches our server more than one times, you can make use of our Idempotency-Key
HTTP header. Make sure that you pass an identifier that uniquely identifies your transaction (e.g. a UUID) and it will be guaranteed that your transaction will be executed at most one time.
- Code
curl --location 'https://api.revenuecat.com/v2/projects/<YOUR_PROJECT_ID>/customers/<YOUR_CUSTOMER_ID>/virtual_currencies/transactions' \
--header 'Idempotency-Key: 2c15a0a5-8cf8-4eb3-95c2-56a343974663' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer sk_***************************' \
--data '{
"adjustments": {
"GLD": 20,
"SLV": 1
}
}'
Virtual Currencies are not transferable
In contrast to regular In-App purchases that can be transferred to other customers during purchase restores, Virtual Currencies are not transferable, and once granted they will remain with the same customer until they are consumed.