### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| Response parameters: | Parameter | Description | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls).optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Android SDK to version 3.7.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```kotlin showLineNumbers val customAssets = AdaptyCustomAssets.of( "hero_image" to AdaptyCustomImageAsset.remote( url = "https://example.com/image.jpg", preview = AdaptyCustomImageAsset.file( FileLocation.fromAsset("images/hero_image_preview.png"), ) ), "hero_video" to AdaptyCustomVideoAsset.file( FileLocation.fromResId(requireContext(), R.raw.custom_video), preview = AdaptyCustomImageAsset.file( FileLocation.fromResId(requireContext(), R.drawable.video_preview), ), ), ) val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, customAssets, ) ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: --- # File: android-present-paywalls --- --- title: "Android - Present new Paywall Builder paywalls" description: "Learn how to present paywalls on Android for effective monetization." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **new Paywall Builder paywalls** only which require SDK v3.0. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **Legacy Paywall Builder paywalls**, check out [Android- Present legacy Paywall Builder paywalls](android-present-paywalls-legacy). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) ::: To get the `viewConfiguration` object used below, see [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls).Insets are the spaces around the paywall that prevent tapable elements from getting hidden behind system bars.
Default: `UNSPECIFIED` which means Adapty will automatically adjust the insets, which works great for edge-to-edge paywalls.
If your paywall isn’t edge-to-edge, you might want to set custom insets. For how to do that, read in the [Change paywall insets](android-present-paywalls#change-paywall-insets) section below.
| | **personalizedOfferResolver** | optional | To indicate personalized pricing ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price) ), implement `AdaptyUiPersonalizedOfferResolver` and pass your own logic that maps `AdaptyPaywallProduct` to true if the product's price is personalized, otherwise false. | | **tagResolver** | optional | Use `AdaptyUiTagResolver` to resolve custom tags within the paywall text. This resolver takes a tag parameter and resolves it to a corresponding string. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | | **timerResolver** | optional | Pass the resolver here if you are going to use custom timer functionality. | :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Change paywall insets Insets are the spaces around the paywall that prevent tapable elements from getting hidden behind system bars. By default, Adapty will automatically adjust the insets, which works great for edge-to-edge paywalls. If your paywall isn’t edge-to-edge, you might want to set custom insets: - If neither the status bar nor the navigation bar overlap with the `AdaptyPaywallView`, use `AdaptyPaywallInsets.NONE`. - For more custom setups, like if your paywall overlaps with the top status bar but not the bottom, you can set only the `bottomInset` to `0`, as shown in the example below:
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](android-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: android-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in Android SDK"
description: "Integrate Adapty SDK into your custom Android paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](android-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](android-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](android-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](android-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](android-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls-android --- --- title: "Render paywall designed by remote config in Android SDK" description: "Discover how to present remote config paywalls in Adapty Android SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values.If the request has been successful, the response contains this object. An [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store. For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter:An [`AdaptyProfile`](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-android --- --- title: "Implement Observer mode in Android SDK" description: "Implement observer mode in Adapty to track user subscription events in Android SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Android](sdk-installation-android#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-android) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. :::1. Implement the `AdaptyUiObserverModeHandler`. The `onPurchaseInitiated` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this callback:
For iOS, StoreKit1: an [`SKPaymentTransaction`](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (`purchase.getOrderId()`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
|For iOS, StoreKit1: an [`SKPaymentTransaction`](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (`purchase.getOrderId()`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| For fullscreen mode where system bars overlap part of your UI, obtain insets in the following way:phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most.An [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level:optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://android.adapty.io/adapty/com.adapty.models/-adapty-onboarding/) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```kotlin Adapty.getOnboardingForDefaultAudience("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val onboarding = result.value // Handle successful onboarding retrieval } is AdaptyResult.Error -> { val error = result.error // Handle error case } } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: android-present-onboardings --- --- title: "Present onboardings in Android SDK" description: "Learn how to present onboardings on Android for effective user engagement." --- Before you start, ensure that: 1. You have installed [Adapty Android SDK](sdk-installation-android.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). If you've customized an onboarding using the Onboarding Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown and how it should be shown. In order to display the visual onboarding on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getOnboardingView()` or create the `OnboardingView` directly:
For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onCustomAction` will be triggered with the action ID from the builder. You can create your own IDs, like "allowNotifications".
```kotlin showLineNumbers
class YourActivity : AppCompatActivity() {
private val eventListener = object : AdaptyOnboardingEventListener {
override fun onCustomAction(action: AdaptyOnboardingCustomAction, context: Context) {
when (action.actionId) {
"allowNotifications" -> {
// Request notification permissions
}
}
}
override fun onError(error: AdaptyOnboardingError, context: Context) {
// Handle errors
}
// ... other required delegate methods
}
}
```
The local fallback paywall JSON is not valid.
Fix your default English paywall, after that replace invalid local paywalls. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix a paywall and to the [Define local fallback paywalls](fallback-paywalls) for details on how to replace the local paywalls.
| |CURRENT_SUBSCRIPTION_TO_UPDATE
\_NOT_FOUND_IN_HISTORY
| The original subscription that needs to be replaced is not found in active subscriptions. | | [BILLING_SERVICE_TIMEOUT](https://developer.android.com/google/play/billing/errors#service_timeout_error_code_-3) | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. | | [FEATURE_NOT_SUPPORTED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#FEATURE_NOT_SUPPORTED()) | The requested feature is not supported by the Play Store on the current device. | | [BILLING_SERVICE_DISCONNECTED](https://developer.android.com/google/play/billing/errors#service_disconnected_error_code_-1) | This error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. | | [BILLING_SERVICE_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#service_unavailable_error_code_2) | This error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. | | [BILLING_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) |This error indicates a billing issue occurred during the purchase process. Possible reasons include:
1. The Play Store app on the user's device is missing or outdated.
2. The user is in an unsupported country.
3. The user is part of an enterprise account where the admin has disabled purchases.
4. Google Play couldn't charge the user's payment method (e.g., an expired credit card).
5. The user is not logged into the Play Store app.
| | [DEVELOPER_ERROR](https://developer.android.com/google/play/billing/errors#developer_error) | This error indicates you're improperly using an API. | | [BILLING_ERROR](https://developer.android.com/google/play/billing/errors#error_error_code_6) | This error indicates an internal problem with Google Play itself. | | [ITEM_ALREADY_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_ALREADY_OWNED()) | The product has already been purchased. | | [ITEM_NOT_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_NOT_OWNED()) | This error indicates that the requested action on the item failed since it is not owned by the user. | | [BILLING_NETWORK_ERROR](https://developer.android.com/google/play/billing/errors#network_error_error_code_12) | This error indicates that there was a problem with the network connection between the device and Play systems. | | NO_PRODUCT_IDS_FOUND |This error indicates that none of the products in the paywall is available in the store.
If you are encountering this error, please follow the steps below to resolve it:
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-android). Use the `getViewConfiguration` method to load the view configuration. ```kotlin showLineNumbers if (!paywall.hasViewConfiguration) { // use your custom logic return } AdaptyUI.getViewConfiguration(paywall) { result -> when(result) { is AdaptyResult.Success -> { val viewConfiguration = result.value // use loaded configuration } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` --- # File: android-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in Android SDK" description: "Present paywalls in Android (Legacy) and manage subscriptions effectively." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide covers the process for **legacy Paywall Builder paywalls** only which requires SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **New Paywall Builder paywalls**, check out [Android - Present new Paywall Builder paywalls](android-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) ::: In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly:
:::note To track subscription events, use [Webhook](webhook) integration in Adapty or integrate directly with your existing service. ::: ## Case 1: Sync subscribers between web and mobile If you use web payment providers like Stripe, ChargeBee, or others, you can sync your subscribers easily. Here's how: 1.
The user’s Adapty profile ID. Visible in the **Adapty ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.
Interchangeable with **adapty-customer-user-id**, use any of them.
| | **adapty-customer-user-id** |The user's ID in your system. Visible in the **Customer user ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.
Interchangeable with **adapty-profile-id**, use any of them.
⚠️ Works only if you
(Required, choose one) The user’s ID in your system. Visible in the **Customer user ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page. Interchangeable with **adapty-profile-id**, use any of them.
⚠️ Works only if you [identify users](identifying-users) in your app code using the Adapty SDK.
| | **adapty-platform** | Specify the app's platform. Possible options: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`. | --- ### Step 2. Change transaction and access management requests In version 1, you used to use: - [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request: to record a transaction and grant or shorten access level. - [Revoke access level](server-side-api-specs-legacy#revoke-subscription-from-a-user) request: to immediately revoke access. They are now replaced with three separate requests to distinguish between adding transactions and managing access levels: 1. **[Grant Access Level](api-adapty/operations/grantAccessLevel):** Use this request to extend an access level without linking it to a transaction. 2. **[Revoke Access Level](api-adapty/operations/revokeAccessLevel):** to immediately revoke or shorten access. 3. **[Set Transaction](api-adapty/operations/setTransaction):** Use this request to add transaction details to Adapty with access levels. --- #### Step 2.1. How to grant access level :::info For a detailed description, refer to the [Grant access level](api-adapty/operations/grantAccessLevel) request. ::: In version 1, the [Prolong/grant a subscription for a user](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request was used to grant access. Now you can grant access with the [Grant access level](api-adapty/operations/grantAccessLevel) request without providing transaction details. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/grant/access-level/` - Parameters to keep: - **access_level_id**: Previously in the endpoint. Required. - **starts_at**: Now nullable. - **expires_at**: Optional for lifetime access and nullable. --- #### Step 2.2. How to revoke or shorten access level :::info For a detailed description, refer to the [Revoke access level](api-adapty/operations/revokeAccessLevel) request. ::: In version 1, you used the [Revoke access level](server-side-api-specs-legacy#revoke-subscription-from-a-user) request to immediately revoke access and the [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request to shorten it. Now you can use the [Revoke access level](api-adapty/operations/revokeAccessLevel) request for both actions. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/purchase/profile/revoke/access-level/` - Parameters to keep: - **access_level_id**: Required. - **expires_at**: Nullable unless access is revoked immediately. --- #### Step 2.3. How to record a subscription transaction :::info For a detailed description, refer to the [Set transaction](api-adapty/operations/setTransaction) request. ::: In version 1, transactions were recorded using the [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request, which was limited to subscription transactions. In version 2, this functionality has been replaced by the [Set Transaction](api-adapty/operations/setTransaction) request. This request can handle both subscription transactions and one-time purchases. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/` - **Details:** The parameters required vary based on whether the transaction is a subscription or a one-time purchase. See the guidelines below for recording subscription transactions. **New fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description** | | --------------------------- | ---------- | ------------- | ------------------ | ------------------ | ------------------------------------------------------------ | | `billing_issue_detected_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_plus_sign: | The datetime when a billing issue was detected (e.g., a failed card charge). Subscription might still be active. This is cleared if the payment goes through. | | `cancellation_reason` | Added | String | :heavy_plus_sign: | :heavy_plus_sign: | Possible reasons for cancellation include: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `upgraded`, or `unknown`. | | `environment` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: |Environment where the transaction took place. Options are `Sandbox` or `Production.`
Replaces the `is_sandbox` parameter.
| | `grace_period_expires_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | The datetime when the [grace period](https://developer.apple.com/news/?id=09122019c) will end, if the subscription is currently in one. | | `is_family_shared` | Added | Boolean | :heavy_minus_sign: | :heavy_minus_sign: | A Boolean value indicating whether the product supports family sharing in App Store Connect. iOS only. Always `false` for iOS below 14.0 and macOS below 11.0. | | `offer` | Added | Object | :heavy_plus_sign: | :heavy_minus_sign: | Represents the purchase offer as an object. See the [Offer](server-side-api-objects#offer) object. | | `originally_purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | For subscription chains, this is the purchase date of the original transaction, linked by `store_original_transaction_id`. | | `purchase_type` | Added | String | :heavy_plus_sign: | :heavy_minus_sign: | Specifies product type, here set to `subscription`. | | `purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | Indicates most recent purchase date. | | `refunded_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | Indicates subscription refund datetime if applicable. | | `renew_status` | Added | Boolean | :heavy_plus_sign: | :heavy_minus_sign: | Indicates if subscription auto-renewal is enabled. | | `renew_status_changed_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | Indicates when auto-renewal status changed. | | `variation_id` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: | The variation ID used to trace purchases to the specific paywall they were made from. | **Removed fields** | **Parameter** | **Change** | **Description** | | ------------------------- | ---------- | ------------------------------------------------------------ | | `base_plan_id` | Removed | Removed. Add the base plan ID to the `store_product_id` field in the format `product_id:base_plan_id`. | | `duration_days` | Removed | Removed as not needed. The duration is calculated automatically. | | `introductory_offer_type` | Removed | Offer types are now in the `offer` object. | | `is_lifetime` | Removed | Removed as it's replaced with the `purchase_type` parameter. | | `is_sandbox` | Removed | Replaced with the `environment` parameter. | | `price_locale` | Removed | Moved to the `price` object. | | `proceeds` | Removed | | | `starts_at` | Removed | Removed as it will be automatically taken from the access level connected to the selected product. | **Changed fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | Change Description | | ------------------------------------------------------------ | ---------------- | --------------- | --------------------------------------- | ------------------ | ------------------------------------------------------------ | | `price` | Changed | Float -> Object | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: | Now represented as a [Price](server-side-api-objects#price) object and includes `price_locale`, `country`, and `value` fields. | | `store` | Changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |Environment where the transaction took place. Options are `Sandbox` or `Production.`
Replaces the `is_sandbox` parameter.
| | `is_family_shared` | Added | Boolean | :heavy_minus_sign: | :heavy_minus_sign: | Indicates whether the product supports family sharing in App Store Connect. iOS only. Always false for iOS below 14.0 and macOS below 11.0. | | `offer` | Added | Object | :heavy_minus_sign: | :heavy_minus_sign: | Represents the purchase offer as an object. See the [Offer](server-side-api-objects#offer) object. | | `purchase_type` | Added | String | :heavy_plus_sign: | :heavy_minus_sign: | Specifies the type of product purchased. Here set to `one_time_purchase`. | | `purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | The datetime when the access level was last purchased. | | `refunded_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | If refunded, shows the datetime of the refund. | | `variation_id` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: | The variation ID used to trace purchases to the specific paywall they were made from. | **Removed fields** | **Parameter** | **Change** | **Description Change** | | ------------------------- | ---------- | ------------------------------------------------------------ | | `base_plan_id` | Removed | Removed. Add the base plan ID to the `store_product_id` field in the format `product_id:base_plan_id`. | | `duration_days` | Removed | Removed as not needed. The duration is calculated automatically. | | `expires_at` | Removed | Removed as not relevant to a one-time purchase. | | `introductory_offer_type` | Removed | Offer types are now in the `offer` object. | | `is_lifetime` | Removed | Removed as it's replaced with the `purchase_type` parameter. | | `is_sandbox` | Removed | Replaced with the `environment`parameter. | | `price_locale` | Removed | Moved to the `price` object. | | `proceeds` | Removed | | | `starts_at` | Removed | Removed as not relevant to a one-time purchase. | **Changed fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description Change** | | ------------------------------------------------------------ | ---------------- | --------------- | --------------------------------------- | ------------------ | ------------------------------------------------------------ | | `price` | Changed | Float -> Object | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: | Now represented as a [Price](server-side-api-objects#price) object and includes `price_locale`, `country`, and `value` fields. | | `store` | Changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::tip
When creating a customer user ID, save it with your user data so you can send the same ID when they log in from new devices or reinstall your app.
:::
```typescript showLineNumbers
try {
await adapty.identify({ customerUserId: "YOUR_USER_ID" });
// successfully identified
} catch (error) {
// handle the error
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new empty profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created on the activation will be automatically linked to the customer user ID.
:::tip
To exclude created empty profiles from the dashboard [analytics](analytics-charts.md), go to **App settings** and set up [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```typescript showLineNumbers
await adapty.activate({
apiKey: "YOUR_PUBLIC_SDK_KEY",
params: {
customerUserId: "YOUR_USER_ID"
}
});
```
### Log users out
If you have a button for logging users out, use the `logout` method. This creates a new anonymous profile ID for the user.
```typescript showLineNumbers
try {
await adapty.logout();
// successful logout
} catch (error) {
// handle the error
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you don't need to do additional setup:
Here's how it works:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), Adapty syncs its transactions automatically.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
---
# File: adapty-cursor-capacitor
---
---
title: "Integrate Adapty into your Capacitor app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your Capacitor app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your Capacitor app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-capacitor.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Capacitor app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `adapty.activate()`.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `adapty.getPaywall()`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the Capacitor SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-capacitor.md](https://adapty.io/docs/adapty-cursor-capacitor.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](capacitor-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](capacitor-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK dependency using npm and activate it with your Public SDK key. This is the foundation — nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-capacitor.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-capacitor.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs on both iOS and Android. Console shows Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params** | optional | Additional parameters for fetching the paywall. | **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-capacitor). In Capacitor SDK, directly call the `createPaywallView` method without manually fetching the view configuration first. :::warning The result of the `createPaywallView` method can only be used once. If you need to use it again, call the `createPaywallView` method anew. ::: ```typescript showLineNumbers if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` Parameters: | Parameter | Presence | Description | | :------------------- | :------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **customTags** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. | | **prefetchProducts** | optional | Enable to optimize the display timing of products on the screen. When `true` AdaptyUI will automatically fetch the necessary products. Default: `false`. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](capacitor-localizations-and-locale-codes). ::: Once you have the view, [present the paywall](capacitor-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```typescript showLineNumbers try { const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from Capacitor SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params** | optional | Additional parameters for fetching the paywall. | ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Capacitor SDK to version 3.8.0 or higher. ::: Here's an example of how you can provide custom asssets via a simple dictionary: ```typescript showLineNumbers const customAssets: Recordoptional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params.fetchPolicy** |optional
default: `'reload_revalidating_cache_data'`
|By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| | **params.loadTimeoutMs** |optional
default: 5000 ms
|This value limits the timeout (in milliseconds) for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeoutMs`, since the operation may consist of different requests under the hood.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```typescript showLineNumbers try { const products = await adapty.getPaywallProducts({ paywall }); // the requested products list } catch (error) { console.error('Failed to fetch products:', error); } ``` Response parameters: | Parameter | Description | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | List of [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price?.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price?.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price?.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscription?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscription?.subscriptionPeriod`. From there you can access the `unit` property to get the length (i.e. 'day', 'week', 'month', 'year', or 'unknown'). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `'month'` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscription?.offer?.phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params.fetchPolicy** |optional
default: `'reload_revalidating_cache_data'`
|By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls-capacitor --- --- title: "Render paywall designed by remote config in Capacitor SDK" description: "Discover how to present remote config paywalls in Adapty Capacitor SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); const headerText = paywall.remoteConfig?.['header_text']; } catch (error) { console.error('Failed to fetch paywall:', error); } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-capacitor#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](capacitor-making-purchases). We recommend [creating a backup paywall called a fallback paywall](capacitor-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```typescript showLineNumbers try { await adapty.logShowPaywall({ paywall }); } catch (error) { console.error('Failed to log paywall view:', error); } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :--------------------------------------------------------- | | **paywall** | required | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object. | --- # File: capacitor-making-purchases --- --- title: "Make purchases in mobile app in Capacitor SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](capacitor-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('User is now subscribed!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------| | **product** | required | An [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **result** | An [`AdaptyPurchaseResult`](https://capacitor.adapty.io/types/adaptypurchaseresult) object with a `type` field indicating the purchase outcome (`'success'`, `'user_cancelled'`, or `'pending'`) and a `profile` field containing the updated [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) on successful purchases. | ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product, params: { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('Subscription updated successfully!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` Additional request parameter: | Parameter | Presence | Description | | :--------- | :------- | :----------------------------------------------------------- | | **params** | optional | An object of the [`MakePurchaseParamsInput`](https://capacitor.adapty.io/types/makepurchaseparamsinput) type containing platform-specific purchase parameters. | The `MakePurchaseParamsInput` structure includes: ```typescript { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } ``` You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ### Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { enablePendingPrepaidPlans: true, }, } }); ``` ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```typescript showLineNumbers try { await adapty.presentCodeRedemptionSheet(); } catch (error) { console.error('Failed to present code redemption sheet:', error); } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: capacitor-restore-purchase --- --- title: "Restore purchases in mobile app in Capacitor SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```typescript showLineNumbers try { const profile = await adapty.restorePurchases(); const isSubscribed = profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Restore access to paid features console.log('Access restored successfully!'); } else { console.log('No active subscriptions found'); } } catch (error) { console.error('Failed to restore purchases:', error); } ``` Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile** | An [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases. Check the **access level status** to determine whether the user has access to the app. | --- # File: implement-observer-mode-capacitor --- --- title: "Implement Observer mode in Capacitor SDK" description: "Implement observer mode in Adapty to track user subscription events in Capacitor SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Capacitor](sdk-installation-capacitor#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-capacitor) from your existing purchase infrastructure to Adapty. ### Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty only for sending subscription events and analytics. :::important When running in the Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { observerMode: true // Enable observer mode } }); } catch (error) { console.error('Failed to activate Adapty:', error); } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | **observerMode** | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-capacitor.md). 2. [Associate paywalls](report-transactions-observer-mode-capacitor) with purchase transactions. --- # File: report-transactions-observer-mode-capacitor --- --- title: "Report transactions in Observer Mode in Capacitor SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Capacitor SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```typescript showLineNumbers const variationId = paywall.variationId; try { await adapty.reportTransaction({ transactionId: 'your_transaction_id', variationId: variationId }); } catch (error) { console.error('Failed to report transaction:', error); } ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | **transactionId** | required |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params.fetchPolicy** |optional
default: `'reload_revalidating_cache_data'`
|By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| | **params.loadTimeoutMs** |optional
default: 5000 ms
|This value limits the timeout (in milliseconds) for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeoutMs`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding** | An [`AdaptyOnboarding`](https://capacitor.adapty.io/interfaces/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```typescript showLineNumbers try { const onboarding = await adapty.getOnboardingForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); console.log('Default audience onboarding fetched successfully'); } catch (error) { console.error('Failed to fetch default audience onboarding:', error); } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **params.fetchPolicy** |optional
default: `'reload_revalidating_cache_data'`
|By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: capacitor-present-onboardings --- --- title: "Present onboardings in Capacitor SDK" description: "Discover how to present onboardings on Capacitor to boost conversions and revenue." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have [created an onboarding](create-onboarding.md). 2. You have added the onboarding to a [placement](placements.md). ## Present onboarding To display an onboarding, use the `view.present()` method on the `view` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createOnboardingView` one more time to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onClose: (actionId, meta) => { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom: (actionId, meta) => { console.log('Custom action:', actionId); return false; // Don't close the onboarding } }); await view.present(); console.log('Onboarding presented successfully'); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `'full_screen'` (default) or `'page_sheet'` values. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `openIn` parameter to `browser_out_app`: ```typescript showLineNumbers await view.present({ openIn: 'browser_out_app' }); // default — browser_in_app ``` ## Next steps Once you've presented your onboarding, you'll want to [handle user interactions and events](capacitor-handling-onboarding-events.md). Learn how to handle onboarding events to respond to user actions and track analytics. --- # File: capacitor-handling-onboarding-events --- --- title: "Handle onboarding events in Capacitor SDK" description: "Handle onboarding-related events in Capacitor using Adapty." --- Onboardings configured with the builder generate events your app can respond to. Use the `setEventHandlers` method to handle these events for standalone screen presentation. Before you start, ensure that: 1. You have [created an onboarding](create-onboarding.md). 2. You have added the onboarding to a [placement](placements.md). ## Set up event handlers To handle events for onboardings, use the `view.setEventHandlers` method: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onAnalytics(event, meta) { console.log('Analytics event:', event); }, onClose(actionId, meta) { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom(actionId, meta) { console.log('Custom action:', actionId); return false; // Don't close the onboarding }, onPaywall(actionId, meta) { console.log('Paywall action:', actionId); view.dismiss().then(() => { openPaywall(actionId); }); }, onStateUpdated(action, meta) { console.log('State updated:', action); }, onFinishedLoading(meta) { console.log('Onboarding finished loading'); }, onError(error) { console.error('Onboarding error:', error); }, }); await view.present(); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Event types The following sections describe the different types of events you can handle. ### Handle custom actions In the builder, you can add a **custom** action to a button and assign it an ID.
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the event handler will be triggered with the `actionId` parameter that matches the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
```typescript showLineNumbers
view.setEventHandlers({
onCustom(actionId, meta) {
switch (actionId) {
case 'login':
console.log('Login action triggered');
break;
case 'allow_notifications':
console.log('Allow notifications action triggered');
break;
}
return false; // Don't close the onboarding
},
});
```
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
```typescript showLineNumbers
view.setEventHandlers({
onClose(actionId, meta) {
console.log('Onboarding closed:', actionId);
return true; // Allow the onboarding to close
},
});
```
This error code indicates that the user canceled a payment request.
No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.
| | paymentInvalid | 3 | This error indicates that one of the payment parameters was not recognized by the store. | | paymentNotAllowed | 4 |This error code indicates that the user is not allowed to authorize payments. Possible reasons:
- Payments are not supported in the user's country.
- The user is a minor.
| | storeProductNotAvailable | 5 | This error code indicates that the requested product is absent from the App Store. Make sure the product is available for the used country. | | cloudServicePermissionDenied | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | cloudServiceNetworkConnectionFailed | 7 | This error code indicates that the device could not connect to the network. | | cloudServiceRevoked | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | privacyAcknowledgementRequired | 9 | This error code indicates that the user has not yet acknowledged the store privacy policy. | | unauthorizedRequestData | 10 | This error code indicates that the request is built incorrectly. | | invalidOfferIdentifier | 11 |The offer identifier is not valid. Possible reasons:
- You have not set up an offer with that identifier in the App Store.
- You have revoked the offer.
- You misprinted the offer ID.
| | invalidSignature | 12 | This error code indicates that the signature in a payment discount is not valid. Make sure you've filled out the **In-app purchase Key ID** field and uploaded the **In-App Purchase Private Key** file. Refer to the [Configure App Store integration](app-store-connection-configuration) topic for details. | | missingOfferParams | 13 |This error indicates issues with Adapty integration or with offers.
Refer to the [Configure App Store integration](app-store-connection-configuration) and to [Offers](offers) for details on how to set them up.
| | invalidOfferPrice | 14 | This error code indicates that the price you specified in the store is no longer valid. Offers must always represent a discounted price. | ## Custom Android codes | Error | Code | Description | |-----|----|-----------| | adaptyNotInitialized | 20 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native]( sdk-installation-reactnative#configure-adapty-sdks). | | productNotFound | 22 | This error indicates that the product requested for purchase is not available in the store. | | invalidJson | 23 | The paywall JSON is not valid. Fix it in the Adapty Dashboard. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix it. | | currentSubscriptionToUpdateNotFoundInHistory | 24 | The original subscription that needs to be renewed is not found. | | pendingPurchase | 25 | This error indicates that the purchase state is pending rather than purchased. Refer to the [Handling pending transactions](https://developer.android.com/google/play/billing/integrate#pending) page in the Android Developer docs for details. | | billingServiceTimeout | 97 | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. | | featureNotSupported | 98 | The requested feature is not supported by the Play Store on the current device. | | billingServiceDisconnected | 99 | This fatal error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. | | billingServiceUnavailable | 102 | This transient error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. | | billingUnavailable | 103 |This error indicates that a user billing error occurred during the purchase process. Examples of when this can occur include:
1\. The Play Store app on the user's device is out of date.
2. The user is in an unsupported country.
3. The user is an enterprise user, and their enterprise admin has disabled users from making purchases.
4. Google Play is unable to charge the user’s payment method. For example, the user's credit card might have expired.
5. The user is not logged into the Play Store app.
| | developerError | 105 | This is a fatal error that indicates you're improperly using an API. | | billingError | 106 | This is a fatal error that indicates an internal problem with Google Play itself. | | itemAlreadyOwned | 107 | The consumable product has already been purchased. | | itemNotOwned | 108 | This error indicates that the requested action on the item failed sin | ## Custom StoreKit codes | Error | Code | Description | |-----|----|-----------| | noProductIDsFound | 1000 |This error indicates that none of the products in the paywall is available in the store.
If you are encountering this error, please follow the steps below to resolve it:
1. Check if all the products have been added to Adapty Dashboard.
2. Ensure that the Bundle ID of your app matches the one from the Apple Connect.
3. Verify that the product identifiers from the app stores match with the ones you have added to the Dashboard. Please note that the identifiers should not contain Bundle ID, unless it is already included in the store.
4. Confirm that the app paid status is active in your Apple tax settings. Ensure that your tax information is up-to-date and your certificates are valid.
5. Check if a bank account is attached to the app, so it can be eligible for monetization.
6. Check if the products are available in all regions.Also, ensure that your products are in **“Ready to Submit”** state.
| | productRequestFailed | 1002 |Unable to fetch available products at the moment. Possible reason:
- No cache was yet created and no internet connection at the same time.
| | cantMakePayments | 1003 | In-App purchases are not allowed on this device. | | noPurchasesToRestore | 1004 | This error indicates that Google Play did not find the purchase to restore. | | cantReadReceipt | 1005 |There is no valid receipt available on the device. This can be an issue during sandbox testing.
No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.
| | productPurchaseFailed | 1006 | Product purchase failed. | | refreshReceiptFailed | 1010 | This error indicates that the receipt was not received. Applicable to StoreKit 1 only. | | receiveRestoredTransactionsFailed | 1011 | Purchase restoration failed. | ## Custom network codes | Error | Code | Description | | :------------------- | :--- | :----------------------------------------------------------- | | notActivated | 2002 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native](sdk-installation-reactnative#configure-adapty-sdks). | | badRequest | 2003 | Bad request. | | serverError | 2004 | Server error. | | networkFailed | 2005 | The network request failed. | | decodingFailed | 2006 | This error indicates that response decoding failed. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've opted it out. Refer to the [Analytics integration](analytics-integration) topic for details. | | wrongParam | 3001 | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation. | | fetchTimeoutError | 3101 | This error means that the paywall could not be fetched within the set limit. To avoid this situation, [set up local fallbacks](fetch-paywalls-and-products). | | operationInterrupted | 9000 | This operation was interrupted by the system. | --- # File: capacitor-sdk-migration-guides --- --- title: "Capacitor SDK Migration Guides" description: "Migration guides for Adapty Capacitor SDK versions." --- This page contains all migration guides for Adapty Capacitor SDK. Choose the version you want to migrate to for detailed instructions: - [**Migrate to v. 3.16**](migration-to-capacitor-316.mdx) --- # File: migration-to-capacitor-316 --- --- title: "Migrate Adapty Capacitor SDK to v. 3.16" description: "Migrate to Adapty Capacitor SDK v3.16 for better performance and new monetization features." --- Starting from Adapty SDK v.3.16.0, Capacitor 8 is required. If you need Capacitor 7, use Adapty SDK v.3.15. To upgrade to Capacitor SDK v.3.16, ensure your project uses Capacitor 8. If you're still using Capacitor 7, you have two options: 1. **Upgrade to Capacitor 8**: Follow the [official Capacitor migration guide](https://capacitorjs.com/docs/updating/8-0) to update your project, then install Adapty SDK v.3.16. 2. **Stay on Adapty SDK v.3.15**: If upgrading to Capacitor 8 is not feasible, continue using Adapty SDK v.3.15, which supports Capacitor 7. --- # End of Documentation _Generated on: 2026-03-05T16:27:48.027Z_ _Successfully processed: 37/37 files_ # FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.029Z Total files: 42 --- # File: sdk-installation-flutter --- --- title: "Install & configure Flutter SDK" description: "Step-by-step guide on installing Adapty SDK on Flutter for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your Flutter app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This optional module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample app](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example), which demonstrates the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements Adapty SDK supports iOS 13.0+, but requires iOS 15.0+ to work properly with paywalls created in the paywall builder. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). :::
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```dart showLineNumbers
try {
await Adapty().identify(customerUserId); // Unique for each user
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```dart showLineNumbers"
try {
await Adapty().activate(
configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY')
..withCustomerUserId(YOUR_CUSTOMER_USER_ID) // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
);
} catch (e) {
// handle the error
}
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```dart showLineNumbers
try {
await Adapty().logout();
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
// handle unknown error
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](flutter-check-subscription-status.md) right after the identification, or [listen for profile updates](flutter-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected
- [**Onboardings**](flutter-onboardings.md): Engage users with onboardings and drive retention
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](flutter-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor-flutter
---
---
title: "Integrate Adapty into your Flutter app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your Flutter app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your Flutter app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-flutter.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Flutter app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty().getPaywall()`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the Flutter SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-flutter.md](https://adapty.io/docs/adapty-cursor-flutter.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](flutter-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](flutter-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK dependency using `flutter pub add` and activate it with your Public SDK key. This is the foundation — nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-flutter.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-flutter.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs on both iOS and Android. Debug console shows Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Once you have the view, [present the paywall](flutter-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from Flutter SDK version 3.2.0. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Flutter SDK to version 3.8.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets:
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](flutter-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: flutter-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in Flutter SDK"
description: "Integrate Adapty SDK into your custom Flutter paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](flutter-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](flutter-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
```dart showLineNumbers
Futureoptional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](flutter-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscription?.localizedPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscription?.period`. From there you can access the `unit` enum to get the length (i.e. day, week, month, year, or unknown). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `AdaptyPeriodUnit.month` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscription?.offer?.phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls-flutter --- --- title: "Render paywall designed by remote config in Flutter SDK" description: "Discover how to present remote config paywalls in Adapty Flutter SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?['header_text']; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-flutter#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](flutter-making-purchases). We recommend [creating a backup paywall called a fallback paywall](flutter-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object. | --- # File: flutter-making-purchases --- --- title: "Make purchases in mobile app in Flutter SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-flutter#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](flutter-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | required | An [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |If the request has been successful, the response contains this object. An [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```dart showLineNumbers try { final result = await adapty.makePurchase( product: product, subscriptionUpdateParams: subscriptionUpdateParams, ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | required | an [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html) object. | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ### Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "Restore purchases in mobile app in Flutter SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```javascript showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Response parameters: | Parameter | Description | |---------|-----------| | **Profile** |An [`AdaptyProfile`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-flutter --- --- title: "Implement Observer mode in Flutter SDK" description: "Implement observer mode in Adapty to track user subscription events in Flutter SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Flutter](sdk-installation-flutter#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-flutter) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-flutter). 3. [Associate paywalls](report-transactions-observer-mode-flutter) with purchase transactions. --- # File: report-transactions-observer-mode-flutter --- --- title: "Report transactions in Observer Mode in Flutter SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Flutter SDK." ---phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: flutter-listen-subscription-changes --- --- title: "Check subscription status in Flutter SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Flutter app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).An [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```javascript showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // handle any changes to subscription state }); ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: flutter-deal-with-att --- --- title: "Deal with ATT in Flutter SDK" description: "Get started with Adapty on Flutter to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-flutter --- --- title: "Kids Mode in Flutter SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in Flutter SDK." --- If your Flutter application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/) and [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` Parameters: | Parameter | Presence | Description | |-----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: flutter-present-onboardings --- --- title: "Present onboardings in Flutter SDK" description: "Learn how to present onboardings effectively to drive more conversions." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Flutter app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty Flutter SDK](sdk-installation-flutter.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Adapty Flutter SDK provides two ways to present onboardings: - **Standalone screen** - **Embedded widget** ## Present as standalone screen To display an onboarding as a standalone screen, use the `onboardingView.present()` method on the `onboardingView` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createOnboardingView` one more time to create a new `onboardingView` instance. :::warning Reusing the same `onboardingView` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```javascript showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Dismiss the onboarding When you need to programmatically close the onboarding, use the `dismiss()` method: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.fullScreen` (default) or `AdaptyUIIOSPresentationStyle.pageSheet` values. ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Embed in widget hierarchy To embed an onboarding within your existing widget tree, use the `AdaptyUIOnboardingPlatformView` widget directly in your Flutter widget hierarchy. ```javascript showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note For Android platform view to work, ensure your `MainActivity` extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## Loader during onboarding When presenting an onboarding, you may notice a short loading screen between your splash screen and the onboarding while the underlying view is being initialized. You can handle this in different ways depending on your needs. #### Control splash screen using onDidFinishLoading :::note This approach is only available when embedding the onboarding as a widget. It is not available for standalone screen presentation. ::: The recommended cross-platform approach is to keep your splash screen or custom overlay visible until the onboarding is fully loaded, then hide it manually. When using the embedded widget, overlay your own widget above it and hide the overlay when `onDidFinishLoading` fires: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### Customize native loader :::important This approach is platform-specific and requires maintaining native UI code. It's not recommended unless you already maintain separate native layers in your app. ::: If you need to customize the default loader itself, you can replace it with platform-specific layouts. This approach requires separate implementations for Android and iOS: - **iOS**: Add `AdaptyOnboardingPlaceholderView.xib` to your Xcode project - **Android**: Create `adapty_onboarding_placeholder_view.xml` in `res/layout` and define a placeholder there ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15.1. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.externalBrowser`:
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onboardingController` will be triggered with the `.custom(id:)` case and the `actionId` parameter is the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
```javascript
// Full-screen presentation
void onboardingViewOnCustomAction(
AdaptyUIOnboardingView view,
AdaptyUIOnboardingMeta meta,
String actionId,
) {
switch (actionId) {
case 'login':
_login();
break;
case 'allow_notifications':
_allowNotifications();
break;
}
}
// Embedded widget
onCustomAction: (meta, actionId) {
_handleCustomAction(actionId);
}
```
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
```javascript showLineNumbers title="Flutter"
// Full-screen presentation
void onboardingViewOnCloseAction(
AdaptyUIOnboardingView view,
AdaptyUIOnboardingMeta meta,
String actionId,
) {
await view.dismiss();
}
// Embedded widget
onCloseAction: (meta, actionId) {
Navigator.of(context).pop();
}
```
2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you're testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments-flutter
---
---
title: "Fix for Code-1003 cantMakePayment error in Flutter SDK"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: flutter-migration-guide-310
---
---
title: "Migration guide to Flutter Adapty SDK 3.10.0"
description: ""
---
Adapty SDK 3.10.0 is a major release that brought some improvements that however may require some migration steps from you:
1. Update the `makePurchase` method to use `AdaptyPurchaseParameters` instead of individual parameters.
2. Replace `vendorProductIds` with `productIdentifiers` in the `AdaptyPaywall` model.
## Update makePurchase method
The `makePurchase` method now uses `AdaptyPurchaseParameters` instead of individual `subscriptionUpdateParams` and `isOfferPersonalized` arguments. This provides better type safety and allows for future extensibility of purchase parameters.
```diff showLineNumbers
- final purchaseResult = await adapty.makePurchase(
- product: product,
- subscriptionUpdateParams: subscriptionUpdateParams,
- isOfferPersonalized: true,
- );
+ final parameters = AdaptyPurchaseParametersBuilder()
+ ..setSubscriptionUpdateParams(subscriptionUpdateParams)
+ ..setIsOfferPersonalized(true)
+ ..setObfuscatedAccountId('your-account-id')
+ ..setObfuscatedProfileId('your-profile-id');
+ final purchaseResult = await adapty.makePurchase(
+ product: product,
+ parameters: parameters.build(),
+ );
```
If no additional parameters are needed, you can simply use:
```dart showLineNumbers
final purchaseResult = await adapty.makePurchase(
product: product,
);
```
## Update AdaptyPaywall model usage
The `vendorProductIds` property has been deprecated in favor of `productIdentifiers`. The new property returns `AdaptyProductIdentifier` objects instead of simple strings, providing more structured product information.
```diff showLineNumbers
- paywall.vendorProductIds.map((vendorId) =>
- ListTextTile(title: vendorId)
- ).toList()
+ paywall.productIdentifiers.map((productId) =>
+ ListTextTile(title: productId.vendorProductId)
+ ).toList()
```
The `AdaptyProductIdentifier` object provides access to the vendor product ID through the `vendorProductId` property, maintaining the same functionality while offering better structure for future enhancements.
## Backward compatibility
Both changes maintain backward compatibility:
- The old parameters in `makePurchase` are deprecated but still functional
- The `vendorProductIds` property is deprecated but still accessible
- Existing code will continue to work, though you'll see deprecation warnings
We recommend updating your code to use the new APIs to ensure future compatibility and take advantage of the improved type safety and extensibility.
---
# File: flutter-migration-guide-38
---
---
title: "Migrate Adapty Flutter SDK to v. 3.8"
description: "Migrate to Adapty Flutter SDK v3.8 for better performance and new monetization features."
---
Adapty SDK 3.8.0 is a major release that brought some improvements which however may require some migration steps from you.
1. Update the observer class and method names.
2. Update the fallback paywalls method name.
3. Update the view class name in event handling methods.
## Update observer class and method names
The observer class and its registration method have been renamed:
```diff showLineNumbers
- class MyObserver extends AdaptyUIObserver {
+ class MyObserver extends AdaptyUIPaywallsEventsObserver {
@override
void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) {
// Handle action
}
}
// Register observer
- AdaptyUI().setObserver(this);
+ AdaptyUI().setPaywallsEventsObserver(this);
```
## Update fallback paywalls method name
The method for setting fallback paywalls has been simplified:
```diff showLineNumbers
try {
- await Adapty.setFallbackPaywalls(assetId);
+ await Adapty.setFallback(assetId);
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
// handle the error
}
```
## Update view class name in event handling methods
All event handling methods now use the new `AdaptyUIPaywallView` class instead of `AdaptyUIView`:
```diff showLineNumbers
- void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action)
+ void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action)
- void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product)
+ void paywallViewDidSelectProduct(AdaptyUIPaywallView view, AdaptyPaywallProduct product)
- void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product)
+ void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product)
- void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile)
+ void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyProfile profile)
- void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error)
+ void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error)
- void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile)
+ void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile)
- void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error)
+ void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error)
- void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error)
+ void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error)
- void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error)
+ void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error)
```
---
# File: migration-to-flutter-sdk-34
---
---
title: "Migrate Adapty Flutter SDK to v. 3.4"
description: "Migrate to Adapty Flutter SDK v3.4 for better performance and new monetization features."
---
Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end.
## Update fallback paywall files
Update your fallback paywall files to ensure compatibility with the new SDK version:
1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard.
2. [Replace the existing fallback paywalls in your mobile app](flutter-use-fallback-paywalls) with the new files.
## Update implementation of Observer Mode
If you're using Observer Mode, make sure to update its implementation.
Previously, different methods were used to report transactions to Adapty. In the new version, the `reportTransaction` method should be used consistently across both Android and iOS. This method explicitly reports each transaction to Adapty, ensuring it's recognized. If a paywall was used, pass the variation ID to link the transaction to it.
:::warning
**Don't skip transaction reporting!**
If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations.
:::
```diff showLineNumbers
- // every time when calling transaction.finish()
- if (Platform.isAndroid) {
- try {
- await Adapty().restorePurchases();
- } on AdaptyError catch (adaptyError) {
- // handle the error
- } catch (e) {
- }
- }
try {
// every time when calling transaction.finish()
await Adapty().reportTransaction(
"YOUR_TRANSACTION_ID",
variationId: "PAYWALL_VARIATION_ID", // optional
);
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
// handle the error
}
```
---
# File: migration-to-flutter330
---
---
title: "Migrate Adapty Flutter SDK to v. 3.3"
description: "Migrate to Adapty Flutter SDK v3.3 for better performance and new monetization features."
---
Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you.
1. Update the method for providing fallback paywalls.
2. Remove `getProductsIntroductoryOfferEligibility` method.
3. Update integration configurations for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh.
4. Update Observer mode implementation.
## Update method for providing fallback paywalls
Previously, the method required the fallback paywall as a JSON string (`jsonString`), but now it takes the path to the local fallback file (`assetId`) instead.
```diff showLineNumbers
import 'dart:async' show Future;
import 'dart:io' show Platform;
-import 'package:flutter/services.dart' show rootBundle;
-final filePath = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json';
-final jsonString = await rootBundle.loadString(filePath);
+final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json';
try {
- await adapty.setFallbackPaywalls(jsonString);
+ await adapty.setFallbackPaywalls(assetId);
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
}
```
For the complete code example, check out the [Use fallback paywalls](flutter-use-fallback-paywalls) page.
## Remove `getProductsIntroductoryOfferEligibility` method
Before Adapty iOS SDK 3.3.0, the product object always included offers, regardless of whether the user was eligible. You had to manually check eligibility before using the offer.
Now, the product object only includes an offer if the user is eligible. This means you no longer need to check eligibility — if an offer is present, the user is eligible.
## Update third-party integration SDK configuration
To ensure integrations work properly with Adapty Flutter SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below.
### Adjust
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration).
```diff showLineNumbers
import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_config.dart';
try {
final adid = await Adjust.getAdid();
if (adid == null) {
// handle the error
}
+ await Adapty().setIntegrationIdentifier(
+ key: "adjust_device_id",
+ value: adid,
+ );
final attributionData = await Adjust.getAttribution();
var attribution = MapA boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | **withCustomerUserId** | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. | | **withIdfaCollectionDisabled** | optional |Set to `true` to disable IDFA collection and sharing.
the user IP address sharing.
The default value is `false`.
For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| | **withIpAddressCollectionDisabled** | optional |Set to `true` to disable user IP address collection and sharing.
The default value is `false`.
| ### Activate AdaptyUI module of Adapty SDK You need to configure the AdaptyUI module only if you plan to use [Paywall Builder](adapty-paywall-builder.md): ```dart showLineNumbers title="Dart" try { final mediaCache = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1, max int value in Dart diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB ); await AdaptyUI().activate( configuration: AdaptyUIConfiguration(mediaCache: mediaCache), observer:A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. At any purchase or restore in your application, you'll need to call `.restorePurchases()` method to record the action in Adapty. The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | idfaCollectionDisabled | optional |A boolean parameter, that allows you to disable IDFA collection for your iOS app. The default value is `false`.
For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| ### Configure Adapty SDK for Android 1. Add the `AdaptyPublicSdkKey` flag into the app’s `AndroidManifest.xml` \(Android) file with the value of your Public SDK key. ```xml showLineNumbers title="AndroidManifest.xml"Contents of the **Public SDK key** field in the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard. **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.
Make sure you use the **Public SDK key** for Adapty initialization, since the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only.
| | AdaptyObserverMode | optional |A boolean value that is controlling [Observer mode](observer-vs-full-mode) . Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | AdaptyIDFACollectionDisabled | optional |A boolean parameter, that allows you to disable IDFA collection for your app. The default value is `false`.
For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| 2. In your application, add: ```javascript showLineNumbers title="Flutter" import 'package:adapty_flutter/adapty_flutter.dart'; ``` 3. Activate Adapty SDK with the following code: ```javascript showLineNumbers title="Flutter" try { Adapty().activate(); } on AdaptyError catch (adaptyError) {} } catch (e) {} ``` Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to display the paywalls and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](flutter-making-purchases) within your app. ### Set up the logging system Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels: | Level | Description | | :------ | :----------------------------------------------------------- | | error | Only errors will be logged. | | warn | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged. | | info | Errors, warnings, and serious information messages, such as those that log the lifecycle of various modules will be logged. | | verbose | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged. | You can set `logLevel` in your app before configuring Adapty. ```javascript showLineNumbers title="Flutter" try { await Adapty().setLogLevel(AdaptyLogLevel.verbose); } on AdaptyError catch (adaptyError) { } catch (e) {} ``` --- # File: flutter-get-legacy-pb-paywalls --- --- title: "Fetch legacy Paywall Builder paywalls in Flutter SDK" description: "Retrieve legacy PB paywalls in your Flutter app with Adapty SDK." --- After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your Flutter app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](flutter-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-flutter). :::optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-flutter). For Flutter, the view configuration is automatically handled when you present the paywall using the `AdaptyUI.showPaywall()` method. --- # File: flutter-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in Flutter SDK" description: "Present paywalls in Flutter (Legacy) with Adapty." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls**, which require Adapty SDK up to version 2.x. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For presenting **New Paywall Builder paywalls**, check out [Flutter - Present new Paywall Builder paywalls](flutter-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). . ::: To display a paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```typescript showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` --- # File: flutter-handling-events-legacy --- --- title: "Handle paywall events in legacy Flutter SDK" description: "Handle subscription events in Flutter (Legacy) with Adapty’s SDK." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with the new Paywall Builder, see [Flutter - Handle paywall events designed with the new Paywall Builder](flutter-handling-events). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIObserver` methods and register the observer before presenting any screen: ```javascript showLineNumbers title="Flutter" AdaptyUI().addObserver(this); ``` ### User-generated events #### Actions If a user has performed some action (`close`, `openURL`, `androidSystemBack`, or `custom`, this method will be invoked: ```javascript showLineNumbers title="Flutter" // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { switch (action.type) { case AdaptyUIActionType.close: view.dismiss(); break; case AdaptyUIActionType.openUrl: final urlString = action.value; if (urlString != null) { launchUrlString(urlString); } default: break; } } ``` The following action types are supported: - `close` - `openUrl` - `custom` - `androidSystemBack`. Note that at the very least you need to implement the reactions to both `close` and `openURL`. For example, if a user taps the close button, the action `close` will occur and you are supposed to dismiss the paywall. Note that `AdaptyUIAction` has optional value property: look at this in the case of `openUrl` and `custom`. > 💡 Login Action > > If you have configured Login Action in the dashboard, you should implement reaction for `custom` action with value `"login"` #### Product selection If a product is selected for purchase (by a user or by the system), this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Started purchase If a user initiates the purchase process, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Canceled purchase If a user initiates the purchase process but manually interrupts it, the function below will be invoked. Basically, this event occurs when the `Adapty.makePurchase()` function completes with the `.paymentCancelled` error: ```javascript showLineNumbers title="Flutter" void paywallViewDidCancelPurchase(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Successful purchase If `Adapty.makePurchase()` succeeds, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) { } ``` #### Failed purchase If `Adapty.makePurchase()` fails, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` #### Successful restore If `Adapty.restorePurchases()` succeeds, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) { } ``` #### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) { } ``` ### Data fetching and rendering #### Product loading errors If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by invoking this method: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) { } ``` #### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) { } ``` In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: flutter-hide-legacy-paywall-builder-paywalls --- --- title: "Hide legacy Paywall Builder paywalls in Flutter SDK" description: "Hide legacy paywalls in your Flutter app with Adapty SDK." --- While Paywall Builder seamlessly handles the purchasing process upon clicking "buy" buttons, you have to manage the closure of paywall screens within your Flutter app. :::warning This guide covers only hiding **legacy Paywall Builder paywalls** which supports Adapty SDK v2.x or earlier. ::: You can hide a paywall screen by calling the `view.dismiss` method. ```dart showLineNumbers try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` --- # End of Documentation _Generated on: 2026-03-05T16:27:48.069Z_ _Successfully processed: 42/42 files_ # IOS - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.071Z Total files: 43 --- # File: sdk-installation-ios --- --- title: "Install & configure iOS SDK" description: "Step-by-step guide on installing Adapty SDK on iOS for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your mobile app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This optional module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: For a complete implementation walkthrough, you can also see the videos:
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a view configuration, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the view configuration is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls). Use the `getPaywallConfiguration` method to load the view configuration. ```swift showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, products: products ) // use loaded configuration } catch { // handle the error } ``` Parameters: | Parameter | Presence | Description | | :----------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. | | **products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](localizations-and-locale-codes). ::: Once loaded, [present the paywall](ios-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note The `getPaywallForDefaultAudience` method is available starting from iOS SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty iOS SDK to version 3.7.0 or higher. ::: Here's an example of how you can provide custom assets via a simple dictionary: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!) ) ), ] let paywallConfig = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, assetsResolver: customAssets ) ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: ## Set up developer-defined timers To use custom timers in your mobile app, create an object that follows the `AdaptyTimerResolver` protocol. This object defines how each custom timer should be rendered. If you prefer, you can use a `[String: Date]` dictionary directly, as it already conforms to this protocol. Here is an example: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` In this example, `CUSTOM_TIMER_NY` and `CUSTOM_TIMER_6H` are the **Timer ID**s of developer-defined timers you set in the Adapty Dashboard. The `timerResolver` ensures your app dynamically updates each timer with the correct value. For example: - `CUSTOM_TIMER_NY`: The time remaining until the timer's end, such as New Year's Day. - `CUSTOM_TIMER_6H`: The time left in a 6-hour period that started when the user opened the paywall. --- # File: ios-present-paywalls --- --- title: "Present new Paywall Builder paywalls in iOS SDK" description: "Discover how to present paywalls on iOS to boost conversions and revenue." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **[new Paywall Builder paywalls](adapty-paywall-builder.md)** . The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **Legacy Paywall Builder paywalls**, check out [iOS - Present legacy Paywall Builder paywalls](ios-present-paywalls-legacy). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) ::: To get the `AdaptyUI.PaywallConfiguration` object used below, see [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls). ## Present paywalls in SwiftUI ### Present as a modal view In order to display the visual paywall on the device screen as a modal view, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration:
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](ios-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: ios-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in iOS SDK"
description: "Integrate Adapty SDK into your custom iOS paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](ios-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](ios-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls --- --- title: "Render paywall designed by remote config in iOS SDK" description: "Discover how to present remote config paywalls in Adapty to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. Don't forget to [check if a user is eligible for an introductory offer in iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) and adjust the paywall view to process the case when they are eligible. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values.If the request has been successful, the response contains this object. An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lower than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## In-app purchases from the App Store When a user initiates a purchase in the App Store and the transaction carries over to your app, you have two options: - **Process the transaction immediately:** Return `true` in `shouldAddStorePayment`. This will trigger the Apple purchase system screen right away. - **Store the product object for later processing:** Return `false` in `shouldAddStorePayment`, then call `makePurchase` with the stored product later. This may be useful if you need to show something custom to your user before triggering a purchase. Here’s the complete snippet: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. The Apple purchase system screen will show automatically. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` when the timing is appropriate func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```swift showLineNumbers Adapty.presentCodeRedemptionSheet() ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: restore-purchase --- --- title: "Restore purchases in mobile app in iOS SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method:An [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: ios-transaction-management --- --- title: "Advanced transaction management in iOS SDK" description: "Finish transactions manually in your iOS app with Adapty SDK." --- :::note Advanced transaction management is supported in the Adapty iOS SDK starting from version 3.12. ::: Advanced transaction management in Adapty gives you more control over how transactions are handled, verified, and finished. Advanced transaction management introduces three optional features that work together: | Feature | Purpose | |-------------------------------------------------------------|----------| | [`appAccountToken`](#assign-appaccounttoken) | Links Apple transactions to your internal user ID | | [`jwsTransaction`](#access-the-jws-representation) | Provides Apple’s signed transaction payload for validation | | [Manual finishing](#control-transaction-finishing-behavior) | Lets you finish transactions only after your backend confirms success | Together, these tools let you build robust custom validation flows while Adapty continues syncing transactions with its backend. :::important Most apps don’t need this. By default, Adapty automatically validates and finishes StoreKit transactions. Use this guide only if you run your own backend validation or want to fully control the purchase lifecycle. ::: ## Assign `appAccountToken` [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `appAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. :::For StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
|
phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```swift showLineNumbers do { builder = try builder.with(customAttribute: "value1", forKey: "key1") } catch { // handle key/value validation error } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```swift showLineNumbers do { builder = try builder.withRemoved(customAttributeForKey: "key2") } catch { // handle error } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: subscription-status --- --- title: "Check subscription status in iOS SDK" description: "Track and manage user subscription status in Adapty for improved customer retention." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level). Before you start checking subscription status, set up [App Store Server Notifications](enable-app-store-server-notifications). ## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](subscription-status#listening-for-subscription-status-updates) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method:An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level:optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://swift.adapty.io/documentation/adapty/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```swift showLineNumbers Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(onboarding): // the requested onboarding case let .failure(error): // handle the error } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: ios-present-onboardings --- --- title: "Present onboardings in iOS SDK" description: "Discover how to present onboardings on iOS to boost conversions and revenue." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty iOS SDK](sdk-installation-ios.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Present onboardings in Swift In order to display the visual onboarding on the device screen, do the following: 1. Get the onboarding view configuration using the `.getOnboardingConfiguration` method. 2. Initialize the visual onboarding you want to display by using the `.onboardingController` method: Request parameters: | Parameter | Presence | Description | |:-----------------------------|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding configuration** | required | An `AdaptyUI.OnboardingConfiguration` object containing all the onboarding properties. Use the `AdaptyUI.getOnboardingConfiguration` method to obtain it. | | **delegate** | required | An `AdaptyOnboardingControllerDelegate` to listen to onboarding events. | Returns: | Object | Description | |:-------------------------------|:--------------------------------------------------------| | **AdaptyOnboardingController** | An object, representing the requested onboarding screen | 3. After the object has been successfully created, you can display it on the screen of the device: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = try AdaptyUI.onboardingController( with: configuration, delegate:
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onboardingController` will be triggered with the `.custom(id:)` case and the `actionId` parameter is the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) {
if action.actionId == "allowNotifications" {
// Request notification permissions
}
}
func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) {
// Handle errors
}
```
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
For example:
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) {
controller.dismiss(animated: true)
}
```
2. Click on the subscription group name. You’ll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don’t match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you’re testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments
---
---
title: "Fix for Code-1003 cantMakePayment error"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: migration-to-ios-315
---
---
title: "Migrate Adapty iOS SDK to v. 3.15"
description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features."
---
If you use [Paywall Builder](adapty-paywall-builder.md) in [Observer mode](observer-vs-full-mode), starting from iOS SDK 3.15, you need to implement a new method `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. This method provides more control over the restore logic, allowing you to handle restore purchases in your custom flow. For complete implementation details, refer to [Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode).
```diff showLineNumbers
func observerMode(didInitiatePurchase product: AdaptyPaywallProduct,
onStartPurchase: @escaping () -> Void,
onFinishPurchase: @escaping () -> Void) {
// use the product object to handle the purchase
// use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase
}
+ func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void,
+ onFinishRestore: @escaping () -> Void) {
+ // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore
+ }
```
---
# File: migration-to-ios-sdk-34
---
---
title: "Migrate Adapty iOS SDK to v. 3.4"
description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features."
---
Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end.
## Update Adapty SDK activation
A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | customerUserId | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. | | idfaCollectionDisabled | optional |Set to `true` to disable IDFA collection and sharing.
the user IP address sharing.
The default value is `false`.
For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| | ipAddressCollectionDisabled | optional |Set to `true` to disable user IP address collection and sharing.
The default value is `false`.
| | logLevel | optional | Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels:optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls). Use the `getViewConfiguration` method to load the view configuration. ```swift showLineNumbers do { guard paywall.hasViewConfiguration else { // use your custom logic return } let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) // use loaded configuration } catch { // handle the error } ``` --- # File: ios-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in iOS SDK" description: "Discover how to present paywalls in iOS using Adapty’s legacy methods." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **New Paywall Builder paywalls**, check out [iOS - Present new Paywall Builder paywalls](ios-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) ::: ## Present paywalls in Swift In order to display the visual paywall on the device screen, you must first configure it. To do this, use the method `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" let visualPaywall = AdaptyUI.paywallController( for:
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```kotlin showLineNumbers
Adapty.identify("YOUR_USER_ID") // Unique for each user
.onSuccess {
// successful identify
}
.onError { error ->
// handle the error
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```kotlin showLineNumbers
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
.build()
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```kotlin showLineNumbers
Adapty.logout()
.onSuccess {
// successful logout
}
.onError { error ->
// handle the error
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](kmp-check-subscription-status.md) right after the identification, or [listen for profile updates](kmp-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](kmp-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor-kmp
---
---
title: "Integrate Adapty into your Kotlin Multiplatform app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your Kotlin Multiplatform app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your Kotlin Multiplatform app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-kmp.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your KMP app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration builder.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the Kotlin Multiplatform SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-kmp.md](https://adapty.io/docs/adapty-cursor-kmp.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](kmp-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](kmp-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK dependency via Gradle and activate it with your Public SDK key. This is the foundation — nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-kotlin-multiplatform.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-kotlin-multiplatform.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs. Logcat (Android) or Xcode console (iOS) shows Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Kotlin Multiplatform: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| Response parameters: | Parameter | Description | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-kmp). Use the `createPaywallView` method to load the view configuration. ```kotlin showLineNumbers if (paywall.hasViewConfiguration) { AdaptyUI.createPaywallView( paywall = paywall, loadTimeout = 5.seconds, preloadProducts = true ).onSuccess { paywallView -> // use paywallView }.onError { error -> // handle the error } } else { // use your custom logic } ``` | Parameter | Presence | Description | | :--------------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | optional | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned. Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. You can use extension functions like `5.seconds` from `kotlin.time.Duration.Companion`. | | **preloadProducts** | optional | Set to `true` to preload products for better performance. When enabled, products are loaded in advance, reducing the time needed to display the paywall. | | **productPurchaseParams** | optional | A map of [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) to [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Use this to configure purchase-specific parameters like personalized offers or subscription update parameters for individual products in the paywall. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder). ::: Once loaded, [present the paywall](kmp-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty SDK to version 3.7.0 or higher. ::: Here's an example of how you can provide custom assets via a map: :::info The Kotlin Multiplatform SDK supports local assets only. For remote content, you should download and cache assets locally before using them in custom assets. ::: ```kotlin showLineNumbers // Import generated Res class for accessing resources viewModelScope.launch { // Get URIs for bundled resources using Res.getUri() val heroImagePath = Res.getUri("files/images/hero_image.png") val demoVideoPath = Res.getUri("files/videos/demo_video.mp4") // Or read image as byte data val imageByteData = Res.readBytes("files/images/avatar.png") // Create custom assets map val customAssets: Map
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
---
# File: kmp-implement-paywalls-manually
---
---
title: "Implement paywalls manually in Kotlin Multiplatform SDK"
description: "Learn how to implement paywalls manually in your Kotlin Multiplatform app with Adapty SDK."
---
## Accept purchases
If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results.
:::important
`makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart).
:::
optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](kmp-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall).onSuccess { products -> // the requested products }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | List of [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscriptionDetails?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionDetails?.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. DAY, WEEK, MONTH, YEAR, or UNKNOWN). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `MONTH` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionDetails?.introductoryOfferPhases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls-kmp --- --- title: "Render paywall designed by remote config in Kotlin Multiplatform SDK" description: "Discover how to present remote config paywalls in Adapty Kotlin Multiplatform SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String // use the remote config values }.onError { error -> // handle the error } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-kmp#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](kmp-making-purchases). We recommend [creating a backup paywall called a fallback paywall](kmp-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```kotlin showLineNumbers Adapty.logShowPaywall(paywall = paywall) .onSuccess { // paywall view logged successfully } .onError { error -> // handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:-------------------------------------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. | --- # File: kmp-making-purchases --- --- title: "Make purchases in mobile app in Kotlin Multiplatform SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-kmp#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](kmp-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```kotlin showLineNumbers Adapty.makePurchase(product = product).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // Grant access to the paid features } } is AdaptyPurchaseResult.UserCanceled -> { // Handle the case where the user canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Handle deferred purchases (e.g., the user will pay offline with cash) } } }.onError { error -> // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------| | **Product** | required | An [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |If the request has been successful, the response contains this object. An [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store. For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```kotlin showLineNumbers val subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( oldSubVendorProductId = "old_subscription_product_id", replacementMode = AdaptyAndroidSubscriptionUpdateReplacementMode.CHARGE_FULL_PRICE ) val purchaseParams = AdaptyPurchaseParameters.Builder() .setSubscriptionUpdateParams(subscriptionUpdateParams) .build() Adapty.makePurchase( product = product, parameters = purchaseParams ).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // successful cross-grade } is AdaptyPurchaseResult.UserCanceled -> { // user canceled the purchase flow } is AdaptyPurchaseResult.Pending -> { // the purchase has not been finished yet, e.g. user will pay offline by cash } } }.onError { error -> // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | |:---------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **parameters** | optional | an [`AdaptyAndroidSubscriptionUpdateParameters`](https://kmp.adapty.io/////adapty/com.adapty.kmp.models/-adapty-android-subscription-update-parameters/) object passed through [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```kotlin showLineNumbers Adapty.presentCodeRedemptionSheet() .onSuccess { // code redemption sheet presented successfully } .onError { error -> // handle the error } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ## Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withGoogleEnablePendingPrepaidPlans(true) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } ``` --- # File: kmp-restore-purchase --- --- title: "Restore purchases in mobile app in Kotlin Multiplatform SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```kotlin showLineNumbers Adapty.restorePurchases().onSuccess { profile -> if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // successful access restore } }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |An [`AdaptyProfile`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-profile/) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-kmp --- --- title: "Implement Observer mode in Kotlin Multiplatform SDK" description: "Implement observer mode in Adapty to track user subscription events in Kotlin Multiplatform SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md). 2. [Report transactions](report-transactions-observer-mode-kmp) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```kotlin showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withObserverMode(true) // default false .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised in observer mode") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-kmp.md). 3. [Associate paywalls](report-transactions-observer-mode-kmp) with purchase transactions. --- # File: report-transactions-observer-mode-kmp --- --- title: "Report transactions in Observer Mode in Kotlin Multiplatform SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Kotlin Multiplatform SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```kotlin showLineNumbers Adapty.reportTransaction( transactionId = "your_transaction_id", variationId = paywall.variationId ).onSuccess { profile -> // Transaction reported successfully // profile contains updated user data }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | required | The transaction ID from your app store purchase. This is typically the purchase token or transaction identifier returned by the store. | | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. | --- # File: kmp-troubleshoot-purchases --- --- title: "Troubleshoot purchases in Kotlin Multiplatform SDK" description: "Troubleshoot purchases in Kotlin Multiplatform SDK" --- This guide helps you resolve common issues when implementing purchases manually in the Kotlin Multiplatform SDK. ## makePurchase is called successfully, but the profile is not being updated **Issue**: The `makePurchase` method completes successfully, but the user's profile and subscription status are not updated in Adapty. **Reason**: This usually indicates incomplete Google Play Store setup or configuration issues. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## makePurchase is invoked twice **Issue**: The `makePurchase` method is being called multiple times for the same purchase. **Reason**: This typically happens when the purchase flow is triggered multiple times due to UI state management issues or rapid user interactions. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## AdaptyError.cantMakePayments in observer mode **Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode. **Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method. **Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode-kmp) for more details. ## Adapty error: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) **Issue**: You're receiving a billing unavailable error from Google Play Store. **Reason**: This error is not related to Adapty. It's a Google Play Billing Library error indicating that billing is not available on the device. **Solution**: This error is not related to Adapty. You can check and find out more about it in the Play Store documentation: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Not found makePurchasesCompletionHandlers **Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found. **Reason**: This is typically related to sandbox testing issues. **Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues. --- # File: kmp-user --- --- title: "Users & access in Kotlin Multiplatform SDK" description: "Learn how to work with users and access levels in your Kotlin Multiplatform app with Adapty SDK." --- This page contains all guides for working with users and access levels in your Kotlin Multiplatform app. Choose the topic you need: - **[Identify users](kmp-identifying-users)** - Learn how to identify users in your app - **[Update user data](kmp-setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](kmp-listen-subscription-changes)** - Monitor subscription changes in real-time - **[Kids Mode](kids-mode-kmp)** - Implement Kids Mode for your app --- # File: kmp-identifying-users --- --- title: "Identify users in Kotlin Multiplatform SDK" description: "Identify users in Adapty to improve personalized subscription experiences." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ### Setting customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("YOUR_USER_ID") .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } } ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### Setting customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID").onSuccess { // successful identify }.onError { error -> // handle the error } ``` Request parameters: - **Customer User ID** (required): a string user identifier. :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ### Logging out and logging in You can log the user out anytime by calling `.logout()` method: ```kotlin showLineNumbers Adapty.logout().onSuccess { // successful logout }.onError { error -> // handle the error } ``` You can then login the user using `.identify()` method. ## Assign `appAccountToken` (iOS) [`iosAppAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `iosAppAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` ## Set obfuscated account IDs (Android) Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics. You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers. :::important You must always pass `androidObfuscatedAccountId` together with `customerUserId`. If you pass only the obfuscated account ID, it will not be included in the transaction. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` --- # File: kmp-setting-user-attributes --- --- title: "Set user attributes in Kotlin Multiplatform SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.FEMALE) .withBirthday(AdaptyProfile.Date(1970, 1, 3)) Adapty.updateProfile(builder.build()) .onSuccess { // profile updated successfully } .onError { error -> // handle the error } ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys `phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `AdaptyProfile.Gender.FEMALE`, `AdaptyProfile.Gender.MALE`, `AdaptyProfile.Gender.OTHER` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withCustomAttribute("key1", "value1") ``` To remove existing key, use `.withRemovedCustomAttribute()` method: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withRemovedCustomAttribute("key2") ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: kmp-listen-subscription-changes --- --- title: "Check subscription status in Kotlin Multiplatform SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Kotlin Multiplatform app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level). Before you start checking subscription status, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). ## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](android-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](android-listen-subscription-changes.md) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> // check the access }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | | --------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile |An [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> if (profile.accessLevels["premium"]?.isActive == true) { // grant access to premium features } }.onError { error -> // handle the error } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```kotlin showLineNumbers Adapty.setOnProfileUpdatedListener { profile -> // handle any changes to subscription state } ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: kmp-deal-with-att --- --- title: "Deal with ATT in Kotlin Multiplatform SDK" description: "Get started with Adapty on Kotlin Multiplatform to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```kotlin showLineNumbers val profileParameters = AdaptyProfileParameters.Builder() .withAttStatus(3) // 3 = ATTrackingManagerAuthorizationStatusAuthorized .build() Adapty.updateProfile(profileParameters) .onSuccess { // ATT status updated successfully } .onError { error -> // handle AdaptyError } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-kmp --- --- title: "Kids Mode in Kotlin Multiplatform SDK" description: "Easily enable Kids Mode to comply with Google policies. No GAID or ad data collected in Kotlin Multiplatform SDK." --- If your Kotlin Multiplatform application is intended for kids, you must follow the policies of [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `optional
default: `en`
| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-onboarding/) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```kotlin showLineNumbers Adapty.getOnboardingForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: kmp-present-onboardings --- --- title: "Present onboardings in Kotlin Multiplatform SDK" description: "Learn how to present onboardings effectively to drive more conversions." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Kotlin Multiplatform app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). To display an onboarding, use the `view.present()` method on the `view` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createOnboardingView(onboarding = onboarding).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createOnboardingView(onboarding = onboarding).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` ## Customize how links open in onboardings By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.EXTERNAL_BROWSER`: ```kotlin showLineNumbers viewModelScope.launch { AdaptyUI.createOnboardingView( onboarding = onboarding, externalUrlsPresentation = AdaptyWebPresentation.EXTERNAL_BROWSER // default – IN_APP_BROWSER ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` --- # File: kmp-handling-onboarding-events --- --- title: "Handle onboarding events in Kotlin Multiplatform SDK" description: "Handle onboarding-related events in Kotlin Multiplatform using Adapty." --- Before you start, ensure that: 1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below. ## Set up the onboarding event observer To handle onboarding events, you need to implement the `AdaptyUIOnboardingsEventsObserver` interface and set it up with `AdaptyUI.setOnboardingsEventsObserver()`. This should be done early in your app's lifecycle, typically in your main activity or app initialization. ```kotlin // In your app initialization AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` ## Custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action.
For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onCustomAction` will be triggered with the action ID from the builder. You can create your own IDs, like "allowNotifications".
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnCustomAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
actionId: String
) {
when (actionId) {
"openPaywall" -> {
// Display paywall from onboarding
// You would typically fetch and present a new paywall here
mainUiScope.launch {
// Example: Get paywall by placement ID
// val paywallResult = Adapty.getPaywall("your_placement_id")
// paywallResult.onSuccess { paywall ->
// val paywallViewResult = AdaptyUI.createPaywallView(paywall)
// paywallViewResult.onSuccess { paywallView ->
// paywallView.present()
// }
// }
}
}
"allowNotifications" -> {
// Handle notification permissions
}
else -> {
// Handle other custom actions
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you're testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments-kmp
---
---
title: "Fix for Code-1003 cantMakePayment error in Kotlin Multiplatform SDK"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: kmp-sdk-migration-guides
---
---
title: "Kotlin Multiplatform SDK Migration Guides"
description: "Migration guides for Adapty Kotlin Multiplatform SDK versions."
---
This page contains all migration guides for Adapty Kotlin Multiplatform SDK. Choose the version you want to migrate to for detailed instructions:
- **[Migrate to v. 3.15](migration-to-kmp-315)**
---
# File: migration-to-kmp-315
---
---
title: "Migration guide to Adapty Kotlin Multiplatform SDK 3.15.0"
description: "Migration steps for Adapty Kotlin Multiplatform SDK 3.15.0"
---
Adapty Kotlin Multiplatform SDK 3.15.0 is a major release that brings new features and improvements which, however, may require some migration steps from you.
1. Update the observer class and method names.
2. Update the fallback paywalls method name.
3. Update the view class name in event handling methods.
## Update observer class and method names
The observer class and its registration method have been renamed:
```diff
- import com.adapty.kmp.AdaptyUIObserver
+ import com.adapty.kmp.AdaptyUIPaywallsEventsObserver
- import com.adapty.kmp.models.AdaptyUIView
+ import com.adapty.kmp.models.AdaptyUIPaywallView
- class MyAdaptyUIObserver : AdaptyUIObserver {
- override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) {
+ class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver {
+ override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
// handle actions
}
}
// Set up the observer
- AdaptyUI.setObserver(MyAdaptyUIObserver())
+ AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver())
```
## Update fallback paywalls method name
The method name for setting fallback paywalls has been changed:
```diff showLineNumbers
- Adapty.setFallbackPaywalls(assetId = "fallback.json")
+ Adapty.setFallback(assetId = "fallback.json")
.onSuccess {
// Fallback paywalls loaded successfully
}
.onError { error ->
// Handle the error
}
```
## Update view class name in event handling methods
All event handling methods now use the new `AdaptyUIPaywallView` class instead of `AdaptyUIView`:
```diff
- override fun paywallViewDidAppear(view: AdaptyUIView) {
+ override fun paywallViewDidAppear(view: AdaptyUIPaywallView) {
// Handle paywall appearance
}
- override fun paywallViewDidDisappear(view: AdaptyUIView) {
+ override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) {
// Handle paywall disappearance
}
- override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) {
+ override fun paywallViewDidSelectProduct(view: AdaptyUIView, productId: String) {
// Handle product selection
}
- override fun paywallViewDidStartPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct) {
+ override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) {
// Handle purchase start
}
- override fun paywallViewDidFinishPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) {
+ override fun paywallViewDidFinishPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) {
// Handle purchase result
}
- override fun paywallViewDidFailPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, error: AdaptyError) {
+ override fun paywallViewDidFailPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError) {
// Add your purchase failure handling logic here
}
- override fun paywallViewDidFinishRestore(view: AdaptyUIView, profile: AdaptyProfile) {
+ override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) {
// Add your successful restore handling logic here
}
- override fun paywallViewDidFailRestore(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your restore failure handling logic here
}
- override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIView, product: AdaptyPaywallProduct?, error: AdaptyError?) {
+ override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError?) {
// Handle web payment navigation result
}
- override fun paywallViewDidFailLoadingProducts(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your product loading failure handling logic here
}
- override fun paywallViewDidFailRendering(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {
// Handle rendering error
}
```
---
# End of Documentation
_Generated on: 2026-03-05T16:27:48.193Z_
_Successfully processed: 41/41 files_
# REACT-NATIVE - Adapty Documentation (Full Content)
This file contains the complete content of all documentation pages for this platform.
Generated on: 2026-03-05T16:27:48.194Z
Total files: 42
---
# File: sdk-installation-react-native-expo
---
---
title: "Install & configure Adapty React Native SDK in an Expo project"
description: "Step-by-step guide on installing Adapty React Native SDK in an Expo project for subscription-based apps."
---
:::important
This guide covers installing and configuring the Adapty React Native SDK **in an Expo project**.
If you’re using **pure React Native (without Expo)**, follow the [React Native installation guide](sdk-installation-react-native-pure.md) instead.
:::
Adapty SDK includes two key modules for seamless integration into your React Native app:
- **Core Adapty**: This module is required for Adapty to function properly in your app.
- **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module.
If you need a full tutorial on how to implement IAP in your React Native app, check [this](https://adapty.io/blog/react-native-in-app-purchases-tutorial/) out.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into an Expo app? Check out our sample apps:
- [Expo dev build sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo) for full functionality including real purchases and Paywall Builder
- [Expo Go & Web sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock) for testing with mock mode
:::
For a complete implementation walkthrough, you can also see the video:
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```typescript showLineNumbers
try {
await adapty.identify("YOUR_USER_ID"); // Unique for each user
// successfully identified
} catch (error) {
// handle the error
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```typescript showLineNumbers
adapty.activate("PUBLIC_SDK_KEY", {
customerUserId: "YOUR_USER_ID" // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
});
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```typescript showLineNumbers
try {
await adapty.logout();
// successful logout
} catch (error) {
// handle the error
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](react-native-check-subscription-status.md) right after the identification, or [listen for profile updates](react-native-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected
- [**Onboardings**](react-native-onboardings.md): Engage users with onboardings and drive retention
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](react-native-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor-react-native
---
---
title: "Integrate Adapty into your React Native app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your React Native app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your React Native app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-react-native.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `adapty.activate("YOUR_PUBLIC_SDK_KEY")`.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `adapty.getPaywall("YOUR_PLACEMENT_ID")`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the React Native SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-react-native.md](https://adapty.io/docs/adapty-cursor-react-native.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](react-native-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](react-native-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK dependency using npm (or yarn) and activate it with your Public SDK key. This is the foundation — nothing else works without it.
We have separate installation guides for Expo and bare React Native projects — pick the one that matches your setup.
**Guides:**
- [Install with Expo](sdk-installation-react-native-expo.md)
- [Install with bare React Native](sdk-installation-react-native-pure.md)
Send this to your LLM (pick the one that matches your setup, or send both):
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-react-native-expo.md
- https://adapty.io/docs/sdk-installation-react-native-pure.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs on both iOS and Android. Metro bundler logs show Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeoutMs** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
| Response parameters: | Parameter | Description | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-react-native). In React Native SDK, directly call the `createPaywallView` method without manually fetching the view configuration first. :::warning The result of the `createPaywallView` method can only be used once. If you need to use it again, call the `createPaywallView` method anew. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```typescript showLineNumbers // for the Adapty SDK < 3.14 – import {createPaywallView} from 'react-native-adapty/dist/ui'; if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { //use your custom logic } ``` Parameters: | Parameter | Presence | Description | | :------------------- | :------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **customTags** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. | | **prefetchProducts** | optional | Enable to optimize the display timing of products on the screen. When `true` AdaptyUI will automatically fetch the necessary products. Default: `false`. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](react-native-localizations-and-locale-codes). ::: Once you have the view, [present the paywall](react-native-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywallForDefaultAudience(id, locale); // the requested paywall } catch (error) { // handle the error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from React Native SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](react-native-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty React Native SDK to version 3.8.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```javascript const customAssets: Record
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](react-native-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: react-native-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in React Native SDK"
description: "Integrate Adapty SDK into your custom React Native paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](react-native-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](react-native-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
```typescript showLineNumbers
async function loadPaywall() {
try {
const paywall: AdaptyPaywall = await adapty.getPaywall('YOUR_PLACEMENT_ID');
const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts(paywall);
// Use products to build your custom paywall UI
} catch (error) {
// Handle the error
}
}
```
## Step 2. Accept purchases
When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile.
```typescript showLineNumbers
async function purchaseProduct(product: AdaptyPaywallProduct) {
try {
const purchaseResult: AdaptyPurchaseResult = await adapty.makePurchase(product);
switch (purchaseResult.type) {
case 'success':
// Purchase successful, profile updated
break;
case 'user_cancelled':
// User canceled the purchase
break;
case 'pending':
// Purchase is pending (e.g., user will pay offline with cash)
break;
}
} catch (error) {
// Handle the error
}
}
```
## Step 3. Restore purchases
App stores require all apps with subscriptions to provide a way users can restore their purchases.
Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile.
```typescript showLineNumbers
async function restorePurchases() {
try {
const profile: AdaptyProfile = await adapty.restorePurchases();
// Restore successful, profile updated
} catch (error) {
// Handle the error
}
}
```
## Next steps
optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](react-native-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](react-native-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeoutMs** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```typescript showLineNumbers try { // ...paywall const products = await adapty.getPaywallProducts(paywall); // the requested products list } catch (error) { // handle the error } ``` Response parameters: | Parameter | Description | | :-------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | List of [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price?.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price?.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price?.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscription?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscription?.subscriptionPeriod`. From there you can access the `unit` property to get the length (i.e. 'day', 'week', 'month', 'year', or 'unknown'). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `'month'` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscription?.offer?.phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](react-native-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
| --- # File: present-remote-config-paywalls-react-native --- --- title: "Render paywall designed by remote config in React Native SDK" description: "Discover how to present remote config paywalls in Adapty React Native SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: "YOUR_PLACEMENT_ID" }); const headerText = paywall.remoteConfig?.["header_text"]; } catch (error) { // handle the error } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-react-native#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](react-native-making-purchases). We recommend [creating a backup paywall called a fallback paywall](react-native-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```typescript showLineNumbers await adapty.logShowPaywall(paywall); ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:--------------------------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) object. | --- # File: react-native-making-purchases --- --- title: "Make purchases in mobile app in React Native SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-react-native#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](react-native-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:-------------------------------------------------------------------------------------------------------------------------------| | **Product** | required | An [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |If the request has been successful, the response contains this object. An [AdaptyProfile](https://react-native.adapty.io/interfaces/adaptyprofile) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product, params); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | | :--------- | :------- | :----------------------------------------------------------- | | **params** | required | an object of the [`MakePurchaseParamsInput`](https://react-native.adapty.io/types/makepurchaseparamsinput) type. | :::info **Version 3.8.2+**: The `MakePurchaseParamsInput` structure has been updated. `oldSubVendorProductId` and `prorationMode` are now nested under `subscriptionUpdateParams`, and `isOfferPersonalized` is moved to the upper level. Example: ```javascript makePurchase(product, { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } }); ``` ::: You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```typescript showLineNumbers adapty.presentCodeRedemptionSheet(); ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ## Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```typescript showLineNumbers adapty.activate("PUBLIC_SDK_KEY", { android: { pendingPrepaidPlansEnabled: true } }); ``` --- # File: react-native-restore-purchase --- --- title: "Restore purchases in mobile app in React Native SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```typescript showLineNumbers try { const profile = await adapty.restorePurchases(); const isSubscribed = profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // restore access } } catch (error) { // handle the error } ``` Response parameters: | Parameter | Description | |---------|-----------| | **Profile** |An [`AdaptyProfile`](https://react-native.adapty.io/interfaces/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-react-native --- --- title: "Implement Observer mode in React Native SDK" description: "Implement observer mode in Adapty to track user subscription events in React Native SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [React Native](sdk-installation-reactnative#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-react-native) from your existing purchase infrastructure to Adapty. ### Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty only for sending subscription events and analytics. :::important When running in the Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { observerMode: true, // Enable observer mode }); ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-react-native.md). 3. [Associate paywalls](report-transactions-observer-mode-react-native) with purchase transactions. --- # File: report-transactions-observer-mode-react-native --- --- title: "Report transactions in Observer Mode in React Native SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in React Native SDK." ---For iOS, StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://react-native.adapty.io/interfaces/adaptypaywall) object. |phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```typescript showLineNumbers try { await adapty.updateProfile({ codableCustomAttributes: { key_1: 'value_1', key_2: 2, }, }); } catch (error) { // handle `AdaptyError` } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```typescript showLineNumbers try { // to remove a key, pass null as its value await adapty.updateProfile({ codableCustomAttributes: { key_1: null, key_2: null, }, }); } catch (error) { // handle `AdaptyError` } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: react-native-listen-subscription-changes --- --- title: "Check subscription status in React Native SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your React Native app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).An [AdaptyProfile](https://react-native.adapty.io/interfaces/adaptyprofile) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); const isActive = profile.accessLevels?.["premium"]?.isActive; if (isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```typescript showLineNumbers // Create an "onLatestProfileLoad" event listener adapty.addEventListener('onLatestProfileLoad', profile => { // handle any changes to subscription state }); ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: react-native-deal-with-att --- --- title: "Deal with ATT in React Native SDK" description: "Get started with Adapty on React Native to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```typescript showLineNumbers try { await adapty.updateProfile({ // you can also pass a string value (validated via tsc) if you prefer appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized, }); } catch (error) { // handle `AdaptyError` } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-react-native --- --- title: "Kids Mode in React Native SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in React Native SDK." --- If your React Native application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/) and [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeoutMs** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://react-native.adapty.io/interfaces/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboardingForDefaultAudience(placementId, locale); // the requested onboarding } catch (error) { // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: react-native-present-onboardings --- --- title: "Present onboardings in React Native SDK" description: "Discover how to present onboardings on React Native to boost conversions and revenue." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty React Native SDK](sdk-installation-reactnative.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Adapty React Native SDK provides two ways to present onboardings: - **React component**: Embedded component allows you to integrate it into your app's architecture and navigation system. - **Modal presentation** ## React component To embed an onboarding within your existing component tree, use the `AdaptyOnboardingView` component directly in your React Native component hierarchy. Embedded component allows you to integrate it into your app's architecture and navigation system.
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the event handler will be triggered with the `actionId` parameter that matches the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you're testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments-react-native
---
---
title: "Fix for Code-1003 cantMakePayment error in React Native SDK"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: migration-react-native-314
---
---
title: "Migrate Adapty React Native SDK to v. 3.14"
description: "Migrate to Adapty React Native SDK v3.14 for better performance and new monetization features."
---
Adapty React Native SDK 3.14.0 is a major release that introduces improvements that require migration steps on your end:
- The `registerEventHandlers` method has been replaced with the `setEventHandlers` method.
- In `AdaptyOnboardingView`, event handlers are now passed as individual props instead of an `eventHandlers` object
- A new, simplified import style has been introduced for UI components
- The `logShowOnboarding` method has been deleted
- The minimum React Native version has been updated to 0.73.0
- The default iOS presentation style for paywalls and onboardings has changed from page sheet to full screen
## Replace `registerEventHandlers` with `setEventHandlers`
The `registerEventHandlers` method used for working with Adapty Paywall and Onboarding Builder has been replaced with the `setEventHandlers` method.
If you use the Adapty Paywall Builder and/or Adapty Onboarding Builder, find `registerEventHandlers` in your app code and replace it with `setEventHandlers`.
The change has been introduced to make the method behavior clearer: Handlers now work one-at-a-time because each returns `true`/`false`, and having multiple handlers for a single event made the resulting behavior unclear.
Note that when using React components like `AdaptyOnboardingView` or `AdaptyPaywallView`, you don't need to return `true`/`false` from event handlers since you control the component's visibility through your own state management. Return values are only needed for modal screen presentation where the SDK manages the view lifecycle.
:::important
Calling `setEventHandlers` multiple times will override the handlers you provide, replacing both default and previously set handlers for those specific events.
:::
```diff showLineNumbers
- const unsubscribe = view.registerEventHandlers({
- // your event handlers
- })
const unsubscribe = view.setEventHandlers({
// your event handlers
})
```
## Update import paths for UI components
Adapty SDK 3.14.0 introduces a simplified import style for UI components. Instead of importing from `react-native-adapty/dist/ui`, you can now import directly from `react-native-adapty`.
The new import style is more consistent with standard React Native practices and makes the import statements cleaner. If you are using UI components like `AdaptyPaywallView` or `AdaptyOnboardingView`, update your imports as shown below:
```diff showLineNumbers
- import { AdaptyPaywallView } from 'react-native-adapty/dist/ui';
+ import { AdaptyPaywallView } from 'react-native-adapty';
- import { AdaptyOnboardingView } from 'react-native-adapty/dist/ui';
+ import { AdaptyOnboardingView } from 'react-native-adapty';
- import { createPaywallView } from 'react-native-adapty/dist/ui';
+ import { createPaywallView } from 'react-native-adapty';
- import { createOnboardingView } from 'react-native-adapty/dist/ui';
+ import { createOnboardingView } from 'react-native-adapty';
```
:::note
For backward compatibility, the old import style (`react-native-adapty/dist/ui`) is still supported. However, we recommend using the new import style for consistency and clarity.
:::
## Update onboarding event handlers in the React component
Event handlers for onboardings have been moved outside the `eventHandlers` object in `AdaptyOnboardingView`. If you are displaying onboardings using `AdaptyOnboardingView`, update the event handling structure.
:::important
Note the way we recommend implementing event handlers. To avoid recreating objects on each render, use `useCallback` for functions that handle events.
:::
```diff showLineNumbers
import React, { useCallback } from 'react';
- import { AdaptyOnboardingView } from 'react-native-adapty/dist/ui';
+ import { AdaptyOnboardingView } from 'react-native-adapty';
+ import type { OnboardingEventHandlers } from 'react-native-adapty';
+
+ function MyOnboarding({ onboarding }) {
+ const onAnalytics = useCallback```diff showLineNumbers - subscriptionDetails?: AdaptySubscriptionDetails; + subscription?: AdaptySubscriptionDetails; ``` 2. [AdaptySubscriptionDetails](https://react-native.adapty.io/interfaces/adaptysubscriptiondetails): - `promotionalOffer` is removed. Now the promotional offer is delivered within the `offer` property only if it's available. In this case `offer?.identifier?.type` will be `'promotional'`. - `introductoryOfferEligibility` is removed (offers are returned only if the user is eligible). - `offerId` is removed. Offer ID is now stored in `AdaptySubscriptionOffer.identifier`. - `offerTags` is moved to `AdaptySubscriptionOffer.android`.
```diff showLineNumbers - introductoryOffers?: AdaptyDiscountPhase[]; + offer?: AdaptySubscriptionOffer; ios?: { - promotionalOffer?: AdaptyDiscountPhase; subscriptionGroupIdentifier?: string; }; android?: { - offerId?: string; basePlanId: string; - introductoryOfferEligibility: OfferEligibility; - offerTags?: string[]; renewalType?: 'prepaid' | 'autorenewable'; }; } ``` 3. [AdaptyDiscountPhase](https://react-native.adapty.io/interfaces/adaptydiscountphase): - The `identifier` field is removed from the `AdaptyDiscountPhase` model. The offer identifier is now stored in `AdaptySubscriptionOffer.identifier`.
```diff showLineNumbers - ios?: { - readonly identifier?: string; - }; ``` ### Remove models 1. `AttributionSource`: - The string is now used in places where `AttributionSource` was previously used. 2. `OfferEligibility`: - This model has been removed as it is no longer needed. Now, an offer is returned only if the user is eligible. ## Remove `getProductsIntroductoryOfferEligibility` method Before Adapty SDK 3.3.1, product objects always included offers, even if the user wasn’t eligible. This required you to manually check eligibility before using the offer. Starting with version 3.3.1, the product object includes offers only if the user is eligible. This simplifies the process, as you can assume the user is eligible if an offer is present. ## Update making purchase In earlier versions, canceled and pending purchases were treated as errors and returned the codes `2: 'paymentCancelled'` and `25: 'pendingPurchase'`, respectively. Starting with version 3.3.1, canceled and pending purchases are now considered successful results and should be handled accordingly: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` ## Update Paywall Builder paywall presentation For updated examples, see the [Present new Paywall Builder paywalls in React Native](react-native-present-paywalls) documentation. ```diff showLineNumbers - import { createPaywallView } from '@adapty/react-native-ui'; + import { createPaywallView } from 'react-native-adapty/dist/ui'; const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` ## Update developer-defined timer implementation Rename the `timerInfo` parameter to `customTimers`: ```diff showLineNumbers - let timerInfo = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } + let customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } //and then you can pass it to createPaywallView as follows: - view = await createPaywallView(paywall, { timerInfo }) + view = await createPaywallView(paywall, { customTimers }) ``` ## Modify Paywall Builder purchase events Previously: - Canceled purchases triggered the `onPurchaseCancelled` callback. - Pending purchases returned error code `25: 'pendingPurchase'`. Now: - Both are handled by the `onPurchaseCompleted` callback. #### Steps to migrate: 1. Remove the `onPurchaseCancelled` callback. 2. Remove error code handling for `25: 'pendingPurchase'`. 3. Update the `onPurchaseCompleted` callback: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ // ... other optional callbacks onPurchaseCompleted(purchaseResult, product) { switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; // highlight-start case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; // highlight-end } // highlight-start return purchaseResult.type !== 'user_cancelled'; // highlight-end }, }); ``` ## Modify Paywall Builder custom action events Removed callbacks: - `onAction` - `onCustomEvent` Added callback: - New `onCustomAction(actionId)` callback. Use it for custom actions. ## Modify `onProductSelected` callback Previously, `onProductSelected` required the `product` object. Now requires `productId` as a string. ## Remove third-party integration parameters from `updateProfile` method Third-party integration identifiers are now set using the `setIntegrationIdentifier` method. The `updateProfile` method no longer accepts them. ## Update third-party integration SDK configuration To ensure integrations work properly with Adapty React Native SDK 3.3.1 and later, update your SDK configurations for the following integrations as described in the sections below. In addition, if you used the `AttributionSource` to get the attribution identifier, change your code to provide the required identifier as a string. ### Adjust Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration). ```diff showLineNumbers import { Adjust, AdjustConfig } from "react-native-adjust"; import { adapty } from "react-native-adapty"; var adjustConfig = new AdjustConfig(appToken, environment); // Before submiting Adjust config... adjustConfig.setAttributionCallbackListener(attribution => { // Make sure Adapty SDK is activated at this point // You may want to lock this thread awaiting of `activate` adapty.updateAttribution(attribution, "adjust"); }); // ... Adjust.create(adjustConfig); + Adjust.getAdid((adid) => { + if (adid) + adapty.setIntegrationIdentifier("adjust_device_id", adid); + }); ``` ### AirBridge Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AirBridge integration](airbridge#sdk-configuration). ```diff showLineNumbers import Airbridge from 'airbridge-react-native-sdk'; import { adapty } from 'react-native-adapty'; try { const deviceId = await Airbridge.state.deviceUUID(); - await adapty.updateProfile({ - airbridgeDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("airbridge_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` ### Amplitude Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Amplitude integration](amplitude#sdk-configuration). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; try { - await adapty.updateProfile({ - amplitudeDeviceId: deviceId, - amplitudeUserId: userId, - }); + await adapty.setIntegrationIdentifier("amplitude_device_id", deviceId); + await adapty.setIntegrationIdentifier("amplitude_user_id", userId); } catch (error) { // handle `AdaptyError` } ``` ### AppMetrica Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppMetrica integration](appmetrica#sdk-configuration). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import AppMetrica, { DEVICE_ID_KEY, StartupParams, StartupParamsReason } from '@appmetrica/react-native-analytics'; // ... const startupParamsCallback = async ( params?: StartupParams, reason?: StartupParamsReason ) => { const deviceId = params?.deviceId if (deviceId) { try { - await adapty.updateProfile({ - appmetricaProfileId: 'YOUR_ADAPTY_CUSTOMER_USER_ID', - appmetricaDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("appmetrica_profile_id", 'YOUR_ADAPTY_CUSTOMER_USER_ID'); + await adapty.setIntegrationIdentifier("appmetrica_device_id", deviceId); } catch (error) { // handle `AdaptyError` } } } AppMetrica.requestStartupParams(startupParamsCallback, [DEVICE_ID_KEY]) ``` ### AppsFlyer Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppsFlyer integration](appsflyer#sdk-configuration). ```diff showLineNumbers import { adapty, AttributionSource } from 'react-native-adapty'; import appsFlyer from 'react-native-appsflyer'; appsFlyer.onInstallConversionData(installData => { try { - const networkUserId = appsFlyer.getAppsFlyerUID(); - adapty.updateAttribution(installData, AttributionSource.AppsFlyer, networkUserId); + const uid = appsFlyer.getAppsFlyerUID(); + adapty.setIntegrationIdentifier("appsflyer_id", uid); + adapty.updateAttribution(installData, "appsflyer"); } catch (error) { // handle the error } }); // ... appsFlyer.initSdk(/*...*/); ``` ### Branch Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Branch integration](branch#sdk-configuration). ```diff showLineNumbers import { adapty, AttributionSource } from 'react-native-adapty'; import branch from 'react-native-branch'; branch.subscribe({ enComplete: ({ params, }) => { - adapty.updateAttribution(params, AttributionSource.Branch); + adapty.updateAttribution(params, "branch"); }, }); ``` ### Facebook Ads Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Facebook Ads integration](facebook-ads#sdk-configuration). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import { AppEventsLogger } from 'react-native-fbsdk-next'; try { const anonymousId = await AppEventsLogger.getAnonymousID(); - await adapty.updateProfile({ - facebookAnonymousId: anonymousId, - }); + await adapty.setIntegrationIdentifier("facebook_anonymous_id", anonymousId); } catch (error) { // handle `AdaptyError` } ``` ### Firebase and Google Analytics Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Firebase and Google Analytics integration](firebase-and-google-analytics). ```diff showLineNumbers import analytics from '@react-native-firebase/analytics'; import { adapty } from 'react-native-adapty'; try { const appInstanceId = await analytics().getAppInstanceId(); - await adapty.updateProfile({ - firebaseAppInstanceId: appInstanceId, - }); + await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } catch (error) { // handle `AdaptyError` } ``` ### Mixpanel Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Mixpanel integration](mixpanel#sdk-configuration). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import { Mixpanel } from 'mixpanel-react-native'; // ... try { - await adapty.updateProfile({ - mixpanelUserId: mixpanelUserId, - }); + await adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelUserId); } catch (error) { // handle `AdaptyError` } ``` ### OneSignal Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for OneSignal integration](onesignal#sdk-configuration).
if you scan a QR code from a CLI dev client it might lead you to this error. To resolve it you can try the following:
> On your device open EAS built app (it should provide some Expo screen) and manually insert the URL that Expo provides (screenshot below). You can unescape special characters in URL with the JS function `unescape(“string”)`, which should result in something like `http://192.168.1.35:8081`
| ### Install Adapty SDKs with Pure React Native If you opt for a purely native approach to manage React Native purchases, please consult the following instructions: 1. In your project, run the installation command: ```sh showLineNumbers title="Shell" yarn add react-native-adapty yarn add @adapty/react-native-ui ``` 2. For iOS: Install required pods: ```sh showLineNumbers title="Shell" pod install --project-directory=ios pod install --project-directory=ios/ ``` The minimum supported iOS version is 13.0, but the [new Paywall Builder](adapty-paywall-builder) requires iOS 15.0 or higher. If you run into an error during pod installation, find this line in your `ios/Podfile` and update the minimum target. After that, you should be able to run `pod install` without any issues. ```diff showLineNumbers title="Podfile" -platform :ios, min_ios_version_supported +platform :ios, 15.0 ``` 2. For Android: Update the `/android/build.gradle` file. Make sure there is the `kotlin-gradle-plugin:1.8.0` dependency or a newer one: ```groovy showLineNumbers title="/android/build.gradle" ... buildscript { ... dependencies { ... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" } } ... ``` ## Configure Adapty SDKs To use Adapty SDKs, import `adapty` and invoke `activate` in your _core component_ such as `App.tsx`. Preferably, position the activation before the React component to ensure no other Adapty calls occur before the activation. ```typescript showLineNumbers title="/src/App.tsx" import { adapty, LogLevel } from 'react-native-adapty'; adapty.activate('PUBLIC_SDK_KEY', { observerMode: false, customerUserId: 'YOUR_USER_ID', logLevel: LogLevel.ERROR, __debugDeferActivation: false, ipAddressCollectionDisabled: false, ios: { idfaCollectionDisabled: false, }, activateUi: true, // NOT necessary as the default value is `true`, but you can pass `false` if you don't use the Paywall Builder mediaCache: { memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1 diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB }, }); const App = () => { // ... } ``` Activation parameters: | Parameter | Presence | Description | |---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | apiKey | required |A Public SDK Key is the unique identifier used to integrate Adapty into your mobile app. You can copy it in the Adapty Dashboard: [**App settings** -> **General **tab -> **API Keys** section](https://app.adapty.io/settings/general).
**SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.
Make sure you use the **Public SDK key** for the Adapty initialization, since the **Secret key** should be used for the [server-side API](getting-started-with-server-side-api) only.
| | observerMode | optional |A boolean value controlling [Observer mode](observer-vs-full-mode) . Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | customerUserId | optional |An identifier of a user in your system. We send it with subscription and analytical events, so we can match events to the right user profile. You can also find customers using the `customerUserId` in the [Profiles](profiles-crm) section.
If you don't have a user ID when you start with Adapty, you can add it later using the `adapty.identify()` method. For more details, see the [Identifying users](react-native-identifying-users) section.
| | logLevel | optional | A string parameter that makes Adapty record errors and other important information to help you understand what's happening. | | \_\_debugDeferActivation | optional | A boolean parameter, that lets you delay SDK activation until your next Adapty call. This is intended solely for development purposes and **should not be used in production**. | | ipAddressCollectionDisabled | optional |Set to `true` to disable user IP address collection and sharing.
The default value is `false`.
For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| | idfaCollectionDisabled | optional | A boolean parameter, that allows you to disable IDFA collection for your iOS app. The default value is `false`. For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section. | Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to display the paywalls and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app. --- # File: react-native-get-legacy-pb-paywalls --- --- title: "Fetch legacy Paywall Builder paywalls in React Native SDK" description: "Retrieve legacy PB paywalls in your React Native app with Adapty SDK." --- After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your React Native app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](react-native-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-react-native). :::optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-react-native). For React Native, the view configuration is automatically handled when you present the paywall using the `adaptyUI.showPaywall()` method. --- # File: react-native-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in React Native SDK" description: "Present paywalls in React Native (Legacy) apps using Adapty." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For presenting **New Paywall Builder paywalls**, check out [React Native - Present new Paywall Builder paywalls](react-native-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). ::: To display a paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```typescript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` **Next step:** - [Handle paywall events](react-native-handling-events-legacy) --- # File: react-native-handling-events-legacy --- --- title: "Handle paywall events in legacy React Native SDK" description: "Handle subscription-related events in React Native (Legacy) with Adapty's event tracking system." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder-legacy) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide covers the process for **legacy Paywall Builder paywalls** only which requires Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with the new Paywall Builder, see [React Native - Handle paywall events designed with new Paywall Builder](react-native-handling-events-1). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the`view.registerEventHandlers` method: ```typescript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCloseButtonPress() { return true; }, onPurchaseCompleted(profile) { return true; }, onPurchaseStarted(product) { /***/}, onPurchaseCancelled(product) { /***/ }, onPurchaseFailed(error) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error) { /***/ }, onProductSelected() { /***/}, onRenderingFailed(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url) { /* handle url */ }, }); ``` You can register event handlers you need, and miss those you do not need. In this case, unused event listeners would not be created. Note that at the very least you need to implement the reactions to both `onCloseButtonPress` and `onUrlPress`. Event handlers return a boolean. If `true` is returned, the displaying process is considered complete, thus the paywall screen closes and event listeners for this view are removed. Note, that `onCloseButtonPress`, `onPurchaseCompleted` and `onRestoreCompleted` in the example above return `true` — This is their default behavior that you can override. ### Event handlers | Event handler | Description | | :-------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCloseButtonPress** | If the close button is visible and a user taps it, this method will be invoked. It is recommended to dismiss the paywall screen in this handler. | | **onPurchaseCompleted** | If a user's purchase succeeds, this method will be invoked and will provide updated `AdaptyProfile`. It is recommended to dismiss the paywall view in this handler. | | **onPurchaseStarted** | If a user taps the "Purchase" action button to start the purchase process, this method will be invoked and will provide `AdaptyPaywallProduct`. | | **onPurchaseCancelled** | If a user initiates the purchase process and manually interrupts it, this method will be invoked and will provide `AdaptyPaywallProduct`. | | **onPurchaseFailed** | If the purchase process fails, this method will be invoked and provide `AdaptyError`. | | **onRestoreCompleted** | If a user's purchase restoration succeeds, this method will be invoked and provide updated `AdaptyProfile`. It is recommended to dismiss the screen if the user has the required `accessLevel`. Refer to the [Subscription status](react-native-listen-subscription-changes) topic to learn how to check it. | | **onRestoreFailed** | If the restoring process fails, this method will be invoked and will provide `AdaptyError`. | | **onProductSelected** | When any product in the paywall view is selected, this method will be invoked, so that you can monitor what the user selects before the purchase. | | **onRenderingFailed** | If an error occurs during view rendering, this method will be invoked and provide `AdaptyError`. Such errors should not occur, so if you come across one, please let us know. | | **onLoadingProductsFailed** | If you haven't set `prefetchProducts: true` in view creation, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, this method will be invoked and provide `AdaptyError`. | --- # End of Documentation _Generated on: 2026-03-05T16:27:48.259Z_ _Successfully processed: 42/42 files_ # TUTORIAL - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.261Z Total files: 224 --- # File: is-adapty-right-for-me --- --- title: "Is Adapty right for me?" description: "Find out how Adapty fits your use case. Whether you're launching a new app, optimizing revenue, or migrating from another tool — here's where to start." --- Adapty is an in-app purchase platform for mobile apps. It handles subscriptions, one-time purchases, and consumables — from purchase processing and receipt validation to analytics, A/B testing, and integrations. Here is how Adapty works for different scenarios. ## I'm launching a new app with in-app purchases Whether you're going to sell subscriptions, one-time purchases, or consumables, Adapty covers the full stack: - **SDKs for 7 platforms**: iOS, Android, React Native, Flutter, Unity, Kotlin Multiplatform, and Capacitor. - **Purchase handling**: Subscriptions with renewals and retry logic, one-time purchases, consumables, and receipt validation — all managed for you. - **No-code paywall builder**: Design and ship paywalls without writing UI code. - **Analytics from day one**: Track revenue, trials, conversions, and more as soon as your first users arrive. Ready to get started? Follow the [Quickstart guide](quickstart). ## I want A/B tests, analytics, and integrations Adapty helps you optimize what's already working: - **A/B testing**: Test different prices, paywall designs, trial lengths, and promotional offers to find what converts best. Use [Growth Autopilot](autopilot) to get a data-driven test plan based on competitor and market analysis. - **Analytics charts**: Track MRR, LTV, churn, retention, and dozens of other metrics. - **Audience segmentation**: Target specific user groups with tailored paywalls and offers. - **Remote paywall configuration**: Iterate on your paywalls without releasing a new app version. - **Third-party integrations**: Send purchase events to Amplitude, AppsFlyer, Adjust, Mixpanel, and other tools your team already uses. Explore [A/B testing](ab-tests), [Analytics](analytics), [Analytics service integrations](analytics-integration), or [Attribution service integrations](attribution-integration). ## I want to implement in-app purchases with an LLM Adapty's docs are optimized for use with AI coding assistants like Cursor, Claude, ChatGPT, and others. Every page is available as plain Markdown, and we provide step-by-step LLM-assisted implementation guides for each platform: - **Copy-paste-ready guides**: Send the guide to your LLM and let it walk you through each implementation stage. - **Markdown access**: Add `.md` to any doc URL or click **Copy for LLM** to get a clean text version. - **Context7 MCP support**: Connect Adapty docs directly to your LLM-powered IDE. Pick your platform and get started: [Integrate Adapty with AI assistance](adapty-cursor). ## I want to run and optimize Apple Ads campaigns If you're running Apple Search Ads, Adapty's Apple Ads Manager connects your campaign performance directly with revenue metrics — no MMP required: - **Real-time performance data**: Track campaigns, ad groups, and keywords. - **End-to-end revenue tracking**: Follow the chain from search to install to trial to subscription to LTV. - **AI predictions and recommendations**: Forecast returns and get scaling suggestions. - **Rule-based automations**: Keep your CPA and ROAS targets stable. Get started with [Apple Ads Manager](adapty-ads-manager). ## I want to track where my users come from Adapty User Acquisition is a built-in attribution solution that connects ad spend with app installs and subscription revenue: - **Unified marketing dashboard**: See ROAS, installs, and revenue across all your channels in one place. - **Built-in attribution**: Connect ad campaigns with installs and revenue without relying on external MMPs. - **Tracking links**: Generate links in Adapty and add them to your campaigns for accurate attribution. - **Deferred deeplinks**: Route users to the right content after install, even if they didn't have the app when they clicked. - **Cohort analysis**: Analyze acquisition performance and user behavior over time. Learn more about [Adapty User Acquisition](adapty-user-acquisition). ## I want to iterate fast without app releases Once Adapty is integrated, most of the day-to-day work happens in the dashboard — no new app versions required: - **No-code paywall builder**: Design and update paywalls in a visual editor, publish changes instantly. - **A/B testing from the dashboard**: Launch experiments, adjust pricing, and swap offers without touching code. - **Dashboard analytics**: Monitor revenue, churn, trials, and conversions in real time. - **Slack and email reports**: Get automated updates on the metrics that matter to your team. Explore the [Paywall Builder](adapty-paywall-builder) or check out [Analytics](charts). ## I sell on the web and need a mobile app If your users already pay through a website, and you're adding a mobile app, Adapty syncs purchases across platforms: - **Stripe and Paddle integration**: Sync web purchases into Adapty automatically. - **Web-to-mobile sync**: Users who paid on the web get access in your app, and vice versa. - **Unified cross-platform analytics**: See web and mobile revenue in one dashboard. Set up [Stripe integration](stripe), [Paddle integration](paddle), or learn how to [sync web and mobile subscribers](sync-subscribers-from-web). ## I'm migrating from another tool Adapty makes it easy to migrate from other subscription platforms: - **Migration guides**: Step-by-step instructions for moving from other subscription platforms. - **Observer Mode**: Keep your existing billing code and adopt Adapty incrementally with [Observer mode](observer-vs-full-mode) — start with analytics and A/B tests, then expand when ready. - **Historical data import**: Bring your existing transaction history into Adapty so your analytics stay complete. Learn about [migrating to Adapty](migrate-to-adapty-from-another-solutions) and [importing historical data](importing-historical-data-to-adapty). --- Still exploring? The [Quickstart guide](quickstart) is always a good place to begin. --- # File: integrate-payments --- --- title: "Integrate with stores or payment platforms" description: "Integrate Adapty with App Store, Google Play, custom stores, Stripe, and Paddle." --- To get started with Adapty, first integrate with the stores where your users buy products. Adapty connects to various app stores and web payment providers, bringing all your in-app purchases and analytics together in one place. ## Integrate with stores and web payments Choose your store below for detailed integration steps: - App Store - Google Play - Web payments: - Stripe - Paddle - Other stores ## Next steps Once you’ve connected your store or payment platform, you can move on to [adding products](quickstart-products.md). --- # File: quickstart-products --- --- title: "Add products" description: "Add in‑app products or subscriptions to Adapty and link them to your App Store, Google Play, Stripe, Paddle, or custom‑store listings." --- Before you can use Adapty’s core features, you need to add each product you sell and link it to every store or payment platform you support. This setup allows you to deliver products to users’ devices and track them in analytics later. In Adapty, anything your app sells is a **product**. If the same item exists in the App Store, Google Play, or Stripe, you can group them into a single product in Adapty. Set it up once and manage it across all platforms from one place. Let's add your first product.
:::important
**The next steps depend on whether you already have products in App Store and/or Google Play:**
:::
5. Click **Save & Continue** and switch to the **App Store** or **Google Play** tab to fill in the product details for the store.
### Design paywall
The easiest way to design a paywall is to create one with the Adapty no-code builder, which requires no design or coding skills. You can choose from a wide array of professionally designed templates or build a fully custom paywall tailored to your app.
:::note
If you don't want to use the paywall builder, you can implement paywalls manually using [remote config](customize-paywall-with-remote-config.md) with custom JSON payloads. Learn more about
Let's design your first paywall. You can craft engaging paywalls with ease:
1. Open **Builder & Generator** on the paywall page.
2. Click **Build no-code paywall**.
3. Choose a template and confirm your choice.
4. Add and customize elements as needed.
5. Click **Save**.
To learn more, go to the detailed article on [Paywall builder](adapty-paywall-builder.md#paywall-elements).
## 2. Add paywall to placement
Now you need to create a
## Next steps
After linking your paywall to a placement in Adapty, the next step is to display it on a device. Let’s move on to [integrating the Adapty SDK](quickstart-sdk.md) into your app.
---
# File: quickstart-sdk
---
---
title: "Integrate the Adapty SDK in your app code"
description: "Integrate Adapty with App Store, Google Play, custom stores, Stripe, and Paddle."
---
Integrate Adapty SDK into your app to:
- Handle purchases, receipt validation, and subscription management out of the box
- Create and test paywalls without app updates
- Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included
- Keep the user subscription status always up to date across app sessions and devices
- Integrate your app with marketing attribution and analytics services using just one line of code
## How does it work
For the basic implementation of the Adapty SDK, you need to take care of three things only:
1. Install and initialize the SDK.
2. Delegate handling in-app purchases to Adapty.
3. Monitor subscription status in the profile. Adapty determines subscription status, type, and expiration – the SDK just consumes this info.
The order and details may vary from app to app, but basically that's it.
## Get started
Choose your platform and dive right in:
**iOS**
- **[SDK Quickstart](ios-sdk-overview.md)**
- **[Sample Apps](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples)**
**Android**
- **[SDK Quickstart](android-sdk-overview.md)**
- **[Sample App](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app)**
**React Native**
- **[SDK Quickstart](react-native-sdk-overview.md)**
- **[Sample Apps](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/)**
**Flutter**
- **[SDK Quickstart](flutter-sdk-overview.md)**
- **[Sample App](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example)**
**Unity**
- **[SDK Quickstart](unity-sdk-overview.md)**
- **[Sample App](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets)**
**Capacitor**
- **[SDK Quickstart](capacitor-sdk-overview.md)**
- **[Sample Apps](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples)**
**Kotlin Multiplatform**:
- **[SDK Quickstart](kmp-sdk-overview.md)**
- **[Sample App](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example)**
## Next steps
Once you've configured the Adapty SDK in the app code, you can move on to [testing the implementation](quickstart-test.md).
---
# File: quickstart-test
---
---
title: "Test your integration with Adapty"
description: "Quickly verify your Adapty integration by testing SDK activation, paywall fetching, and in-app purchases on App Store, Google Play, Stripe, and Paddle."
---
You're all set! Now make sure your integration works as intended and that you can see your purchases in the Adapty dashboard.
Running a test purchase is the best way to verify your integration works end-to-end. Start with an in-app purchase, then validate your results.
## 1. Test in-app purchases
Follow the guide based on your store or payment platform.
### App store
We recommend using a test account (Sandbox Apple ID) and conducting testing on a real device. To learn more about all testing steps, go to the detailed article on [App Store Sandbox testing](test-purchases-in-sandbox.md).
:::warning
Test on a real device for the most reliable results. You can optionally test using the simulator, but we don’t recommend it as less reliable.
:::
### Google Play Store
Create a test user and test your app on a real device. To learn more about all testing steps, go to the detailed article on [Google Play Store testing](testing-on-android.md).
:::note
Google [recommends](https://support.google.com/googleplay/android-developer/answer/14316361) using a real device for testing. If you do decide to use an emulator, make sure that it has Google Play installed to ensure that your app is functioning properly.
:::
### Stripe
Testing purchases on Stripe requires connecting Stripe to Adapty using the API key for Stripe Test mode. Transactions that you make from Stripe's Test mode will be considered Sandbox in Adapty.
To learn more about all connection steps, go to the [Stripe integration article](stripe.md#6-test-your-integration).
### Paddle
Testing purchases on Paddle requires connecting Paddle to Adapty using the API key for Paddle test environment. Transactions that you make from Paddle's test environment will be considered Test in Adapty.
To learn more about all connection steps, go to the [Paddle integration article](paddle.md#4-test-your-integration).
## 2. Validate test purchases
After making a test purchase, check for the corresponding transaction in the [**Event Feed**](https://app.adapty.io/event-feed) in the Adapty Dashboard. If the purchase doesn't appear in the **Event Feed**, Adapty is not tracking it.
Learn more in the detailed guide on [validating test purchases](validate-test-purchases.md).
## Next steps
Congratulations on onboarding Adapty successfully! Now you're ready to grow your in-app purchases.
Prepare for the production release:
Or, you can proceed with the following:
- **[A/B testing](ab-tests.md)**: Experiment with different prices, subscription durations, trial periods, and visual elements to identify the most effective combinations.
- **[Analytics](how-adapty-analytics-works.md)**: Dive into detailed monetization metrics to understand user behavior and optimize revenue performance.
- **Integrations**: Adapty sends [subscription events](events.md) to third party analytics and attribution tool, such as [Amplitude](amplitude), [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Mixpanel](mixpanel), [Facebook Ads](facebook-ads), [AppMetrica](appmetrica), and a custom [Webhook](webhook).
Feasible, but requires a significant amount of additional coding and configuration, more than in Full Mode.
| ✅ | | **Implementation Time** |For analytics and integrations: Less than an hour
With A/B tests: Up to a week with thorough testing
| Several hours | ## How Observer Mode works In Observer mode, you report new transactions from Apple/Google to the Adapty SDK, and Adapty SDK forwards them to the Adapty backend. You are tasked with managing access to the paid content in their app, completing transactions, handling renewals, addressing billing issues, and so on. ## How to set up Observer mode 1. Set up initial integration of Adapty [with Google Play](initial-android) and [with App Store](initial_ios). 2. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [iOS](sdk-installation-ios#configure-adapty-sdk), [Android](sdk-installation-android#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), [Flutter](sdk-installation-flutter#configure-adapty-sdk), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md#configure-adapty-sdk), and [Unity](sdk-installation-unity#configure-adapty-sdk). 3. [Report transactions](report-transactions-observer-mode) from your existing purchase infrastructure to Adapty for iOS and iOS-based cross-platform frameworks. 4. (optional) If you want to use 3d-party integrations, set them up as described in the [Configure 3d-party integration](configuration) topic. :::warning When operating in Observer mode, the Adapty SDK does not finalize transactions, so ensure you handle this aspect yourself. ::: ## How to use paywalls and A/B tests in Observer mode In Observer mode, Adapty SDK cannot determine the source of purchases as you make them in your own infrastructure. Therefore, if you intend to use paywalls and/or A/B tests in Observer mode, you need to associate the transaction coming from your app store with the corresponding paywall in your mobile app code when you report a transaction. Additionally, paywalls designed with Paywall Builder should be displayed in a special way when using the Observer mode: - Display paywalls in Observer mode for [iOS](implement-observer-mode) or [Android](android-present-paywall-builder-paywalls-in-observer-mode). - [Associate paywalls to purchase transactions](report-transactions-observer-mode) when reporting transactions in Observer mode. --- # File: migration-from-revenuecat --- --- title: "Migration from RevenueCat" description: "Migrate from RevenueCat to Adapty with our step-by-step guide." --- Your migration plan will have 5 logical steps and take an average of 2 hours. 90% of all migrations take less than one working day. 1. Learn the core differences; create and prepare an Adapty account _(5 minutes)_; 2. Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md), [Unity](sdk-installation-unity)) instead of RevenueCat SDK _(1 hour)_; 3. Set up [Apple App Store server notifications](enable-app-store-server-notifications) to Adapty and (optionally) [raw events forwarding](enable-app-store-server-notifications#raw-events-forwarding) _(5 minutes)_; 4. Test and release updates of your app _(30 minutes);_ 5. (Optional) Ask RevenueCat support for historical data in CSV format _(5 minutes);_ 6. (Optional) Import historical data via Adapty support _(30 minutes)_. :::info Your subscribers will migrate automatically All users who have ever activated a subscription will instantly move to Adapty as soon as they open a new version of your app with Adapty SDK. The subscription status validation and premium access will be restored automatically. ::: Before you push a new version of your app with Adapty SDK, make sure to check our [release сhecklist](release-checklist). ### Learn the core differences; create and prepare an Adapty account Adapty and RevenueCat SDKs are similarly designed. The biggest difference is the network usage and the speed: Adapty SDK is designed to provide you with information on demand as fast as possible when you ask for it. For example, when requesting a paywall, you get the [remote config](customize-paywall-with-remote-config) first to pre-build your onboarding or paywall and then request products in a dedicated request. Naming is slightly different: | RevenueCat | Adapty | | :---------- | :-------------- | | Package | Product | | Offering | Paywall | | Paywall | Paywall Builder | | Entitlement | Access level | Adapty has a concept of [placement](placements). It's a logical place inside your app where the user can make a purchase. In most cases, you have one or two placements: - Onboarding (because 80% of all purchases take place there); - General (you show it in settings or inside the app after the onboarding).
### Install Adapty SDK and replace RevenueCat SDK
Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md), [Unity](sdk-installation-unity)) in your app.
You need to replace a couple of SDK methods on the app side. Let's look at the most common functions and how to replace them with Adapty SDK.
#### SDK activation
Replace `Purchases.configure` with `Adapty.activate`.
#### Getting paywalls (offerings)
Replace `Purchases.shared.getOfferings` with [`Adapty.getPaywall`](fetch-paywalls-and-products#fetch-paywall-information).
In Adapty, you always request the paywall via [placement id](placements). In practice, you only fetch no more than 1-2 paywalls, so we made this on purpose to speed up the SDK and reduce network usage.
#### Getting a user (customer profile)
Replace `Purchases.shared.getCustomerInfo` with `Adapty.getProfile`.
#### Getting products
In RevenueCat, you use the following structure:`Purchases.shared.getOfferings` and then `self.offering?.availablePackages`.
In Adapty, you first request a paywall (read above) to get immediate access to Adapty's [remote config](customize-paywall-with-remote-config) and then call for products with [`Adapty.getPaywallProducts`](fetch-paywalls-and-products#fetch-products).
#### Making a purchase
Replace `Purchases.shared.purchase` with [`Adapty.makePurchase`](making-purchases#make-purchase).
#### Checking access level (entitlement)
Get a customer profile (read above first) and then replace
`customerInfo?.entitlements["premium"]?.isActive == true`
with
[`profile.accessLevels["premium"]?.isActive == true`](subscription-status#retrieving-the-access-level-from-the-server).
#### Restore purchase
Replace `Purchases.shared.restorePurchases` with [`Adapty.restorePurchases`](restore-purchase).
#### Check if the user is logged in
Replace `Purchases.shared.isAnonymous` with `if profile.customerUserId == nil`.
#### Log in user
Replace `Purchases.shared.logIn` with [`Adapty.identify`](identifying-users#setting-customer-user-id-after-configuration).
#### Log out user
Replace `Purchases.shared.logOut` with [`Adapty.logout`](identifying-users#logging-out-and-logging-in).
### Switch App Store server-side notifications to Adapty
Read how to do this [here](migrate-to-adapty-from-another-solutions#changing-apple-server-notifications).
### Test and release a new version of your app
If you're reading this, you've already:
- [x] Configured Adapty Dashboard
- [x] Installed Adapty SDK
- [x] Replaced SDK logic with Adapty functions
- [x] Switched App Store server-side notifications to Adapty and optionally turn on raw events forwarding to RevenueCat
- [ ] Made a sandbox purchase
- [ ] Made a new app release
If you checked the points above, just make a test purchase in the Sandbox and then release the app.
:::info
Go through [release checklist](release-checklist).
Make the final check using our list to validate the existing integration or add additional features such as [attribution](attribution-integration) or [analytics](analytics-integration) integrations.
:::
### (Optional) Export your RevenueCat historical data in CSV format
:::warning
Don't rush the historical data import
You should wait for at least a week after the release with the SDK before doing historical data import. During that time we will get all the info about purchase prices from the SDK, so the data you import will be more relevant.
:::
Export your historical data from RevenueCat in CSV format by following the instructions in [RevenueCat’s official documentation](https://www.revenuecat.com/docs/integrations/scheduled-data-exports).
### (Optional) Ask RevenueCat support for Google Purchase Tokens
If you need to import Google Play transactions, contact RevenueCat support for a CSV file containing Google Purchase Tokens via their [support page](https://app.revenuecat.com/settings/support). The Google Purchase Token is a unique identifier provided by Google Play for each transaction, essential for accurately tracking and verifying purchases in Adapty. This information is not included in the standard export file. The file contains the following three columns:
- `user_id`
- `google_purchase_token`
- `google_product_id`
### Write us to import your historical data
Contact us via the website messenger or email us at [support@adapty.io](mailto:support@adapty.io) with your CSV files.
1. Send the CSV file you exported from RevenueCat directly to our support team.
2. If importing Google Play transactions, include the CSV file with Google Purchase Tokens that you received from RevenueCat support.
3. Let us know which user ID should be used as the Customer User ID (Adapty’s primary user identifier): `rc_original_app_user_id` or `rc_last_seen_app_user_id_alias`.
Our Support Team will import your transactions to Adapty. The following data will imported to Adapty for every transaction:
| Parameter | Description |
| ----------------------------- | ------------------------------------------------------------ |
| user_id | Customer User ID, the main identifier of your user in Adapty and your system. |
| apple_original_transaction_id | For subscription chains, this is the original transaction's purchase date, linked by `store_original_transaction_id`. |
| google_product_id | The product ID in the Google Play Store. |
| google_purchase_token | A unique identifier provided by Google Play for each transaction, required for validation. |
| country | The country of the user. |
| created_at | The date and time of the user creation. |
| subscription_expiration_date | The date and time when the subscription expires. |
| email | The end user's email. |
| phone_number | The end user's phone number. |
| idfa | The Identifier for Advertisers (IDFA), assigned by Apple to a user's device. |
| idfv | The Identifier for Vendors (IDFV), a code assigned to all apps by one developer and shared across those apps on a device. |
| advertising_id | A unique identifier provided by the Android OS that advertisers may use for tracking. |
| attribution_channel | The marketing channel name. |
| attribution_campaign | The marketing campaign name. |
| attribution_ad_group | The attribution ad group. |
| attribution_ad_set | The attribution ad set. |
| attribution_creative | The attribution creative keyword. |
In addition, integration identifiers for the following integrations will be imported: Amplitude, Mixpanel, AppsFlyer, Adjust, and FacebookAds.
### FAQ
#### I successfully installed Adapty SDK and released a new app version with it. What will happen to my legacy subscribers who did not update to a version with Adapty SDK?
Most users charge their phones overnight, it's when the App Store usually auto-updates all their apps, so it shouldn't be a problem. There may still be a small number of paid subscribers who did not upgrade, but they will still have access to the premium content. You don't need to worry about it and force them to update.
#### Do I need to export my historical data from RevenueCat as quickly as possible, or will I lose it?
You don't need to make it super fast; make a release with Adapty SDK first, and then give us your historical data. We will restore the history of your users' payments and fill in [profiles](profiles-crm) and [charts](charts).
#### I use MMP (AppsFlyer, Adjust, etc.) and analytics (Mixpanel, Amplitude, etc.). How do I make sure that everything will work?
You first need to pass us the IDs of such 3rd party services via our SDK that you want us to send data to. Read the guide for [attribution integration](attribution-integration) and for [analytics integration](analytics-integration). For historical data and legacy users, **make sure you pass us those IDs from the data you exported from RevenueCat.**
---
# File: importing-historical-data-to-adapty
---
---
title: "Importing historical data to Adapty"
description: "Import historical data into Adapty for detailed analytics."
---
After installing the Adapty SDK and releasing your app, you can access your users and subscribers in the [Profiles](profiles-crm) section. But what if you have legacy infrastructure and need to migrate to Adapty, or simply want to see your existing data in Adapty?
:::note
Data import is not mandatory
Adapty will automatically grant access levels to historical users and restore their purchase events once they open the app with the Adapty SDK integrated. For this use case, importing historical data is not necessary. However, importing data ensures precise analytics if you have a significant number of historical transactions, although it is generally not required for migration.
:::
To import data to Adapty:
1. Export your transactions to a CSV file (separate files should be provided for iOS, Android, and Stripe). Please refer to the [Import file format section](importing-historical-data-to-adapty#import-file-format) below for detailed requirements.
2. If any file exceeds 1 GB, prepare a data sample with approximately 100 lines.
3. Upload all the files to Google Drive (you can compress them, but keep them separate).
4. For iOS transactions, ensure the **In-app purchase API** section in the [**App settings**](https://app.adapty.io/settings/ios-sdk)is filled out with the **Issuer ID**, **Key ID**, and the **Private key** (.P8 file) even if you use the StoreKit 1. See the [Provide Issuer ID and Key ID](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) and [Upload In-App Purchase Key file](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) sections for detailed instructions.
5. Share the links with our team via [email](mailto:support@adapty.io) or through the online chat in the Adapty Dashboard.
Do not worry, importing historical data will not create duplicates, even if that data overlaps with existing entries in Adapty.
## Known limitations for Android
1. Only active subscriptions will be restored; expired transactions will not be.
2. Only the latest renewals in a subscription will be restored; the entire chain of purchases will not be.
3. If the product price has changed since the purchase, the current price will be used, which may result in incorrect pricing.
:::note
If you have a large volume of Android transactions, you may need to [request a Google Play Developer API quota increase](google-play-quota-increase) before starting the import to avoid exceeding the default API limit.
:::
## Import file format
Please prepare your data in a file or files that meet the following rules:
- [ ] The file format is .CSV.
- [ ] Separate files for Android, iOS, and Stripe imports.
- [ ] Every import file contains all [required columns](importing-historical-data-to-adapty#required-fields).
- [ ] The columns in the import file(s) have headers.
- [ ] The column headers are exactly as in the **Column name** column in the table below. Please check for typos.
- [ ] Columns that are not required can be absent from the file. Don't add empty columns for data you don't have.
- [ ] Import files should not have extra columns not mentioned in the table. If present, please delete them.
- [ ] Values are separated by commas.
- [ ] Values are not enclosed in quotes.
- [ ] If there are several **apple_original_transaction_id**'s for one user, add all of them as separate lines for each **apple_original_transaction_id**. Otherwise, we may not be able to restore consumable purchases.
Please use the following files as samples for [iOS](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/adapty_import_ios_sample.csv) and [Android](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/adapty_import_android_sample.csv).
### Available import file columns
| Column name | Presence | Description |
|-----------|--------|-----------|
| **user_id** | required | ID of your user |
| **apple_original_transaction_id** | required for iOS | The original transaction ID or OTID ([learn more](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid) ), used in StoreKit 2 import mechanism. As one user can have multiple OTIDs, it is enough to provide at least one for successful import.
**Note:** We require In-app purchase API credentials for this import to be set up in your Adapty Dashboard. Learn how to do it [here](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) .
| | **google_product_id** | required for Google | Product ID in the Google Play Store. | | **google_purchase_token** | required for Google | A unique identifier that represents the user and the product ID for the in-app product they purchased | | **google_is_subscription** | required for Google | Possible values are `1` \| `0` | | **stripe_token** | required for Stripe | Token of a Stripe object that represents a unique purchase. Could either be a token of Stripe's Subscription (`sub_...`) or Payment Intent (`pi_...`). | | **subscription_expiration_date** | optional | The date of subscription expiration, i.g. next charging date, date, and time with timezone (2020-12-31T23:59:59-06:00) | | **created_at** | optional | Date and time of profile creation (2019-12-31 23:59:59-06:00) | | **birthday** | optional | The birthday of the user in format 2000-12-31 | | **email** | optional | The e-mail of your user | | **gender** | optional | The gender of the user | | **phone_number** | optional | The phone number of your user | | **country** | optional | format [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) | | **first_name** | optional | The first name of your user | | **last_name** | optional | The last name of your user | | **last_seen** | optional | The date and time with timezone (2020-12-31T23:59:59-06:00) | | **idfa** | optional | The identifier for advertisers (IDFA) is a random device identifier assigned by Apple to a user's device. Applicable to iOS apps only | | **idfv** | optional | The identifier for vendors (IDFV) is a unique code assigned to all apps developed by a single developer, which in this case refers to your apps. Applicable to iOS apps only | | **advertising_id** | optional | The Advertising ID is a unique code assigned by the Android Operating System that advertisers might use to uniquely identify a user's device | | **amplitude_user_id** | optional | The user ID from Amplitude | | **amplitude_device_id** | optional | The device ID from Amplitude | | **mixpanel_user_id** | optional | User ID from Mixpanel | | **appmetrica_profile_id** | optional | User profile ID from AppMetrica | | **appmetrica_device_id** | optional | The device ID from AppMetrica | | **appsflyer_id** | optional | Unique identifier from AppsFlyer | | **adjust_device_id** | optional | The device ID from Adjust | | **facebook_anonymous_id** | optional | A unique identifier generated by Facebook for users who interact with your app or website anonymously, meaning they are not logged into Facebook | | **branch_id** | optional | Unique identifier from Branch | | **attribution_source** | optional | The source integration of the attribution, for example, appsflyer | | **attribution_status** | optional | organic | | **attribution_channel** | optional | The attribution channel that brought the transaction | | **attribution_campaign** | optional | The attribution campaign that brought the transaction | | **attribution_ad_group** | optional | The attribution ad group that brought the transaction | | **attribution_ad_set** | optional | The attribution ad set that brought the transaction | | **attribution_creative** | optional | Specific visual or textual elements used in an advertisement or marketing campaign that are tracked to determine their effectiveness in driving desired actions, such as clicks, conversions, or installs | | **custom_attributes** | optional | Define up to 30 custom attributes as a JSON dictionary in key-value format:Format: `"{'string_value': 'some_value', 'float_value': 123.0, 'int_value': 456}"`.
Note the use of double and single quotes in the format. Keep in mind that booleans and integers will be converted to floats.
| ### Required Fields There are 2 groups of required fields for each platform: **user_id** and data identifying purchases specific to the corresponding platform. Refer to the table below for the mandatory fields per platform. | Platform | Required fields | |--------|---------------| | iOS |user_id
apple_original_transaction_id
| | Android |user_id
google_product_id
google_purchase_token
google_is_subscription
| | Stripe |user_id
stripe_token
| Without these fields, Adapty won't be able to fetch transactions. For precise cohort analytics, please specify `created_at`. If not provided, we will assume the install date to be the same as the first purchase date. ### Import data to Adapty Please contact us and share your import files via [support@adapty.io](mailto:support@adapty.io) or through the online chat in the [Adapty Dashboard](https://app.adapty.io/overview). --- # File: whats-new --- --- title: "What's new" description: "Stay updated with the latest features and improvements in Adapty" --- Discover the latest features, improvements, SDK updates, and documentation enhancements that help you optimize your app's monetization strategy. This page highlights the most important releases each month. :::note Have feedback on new features? We'd love to hear from you! Contact us via the [Product feedback board](https://adapty.featurebase.app/en?b=69831ba5e82e7a3391632ec2). ::: ## February 2026 - **Country-specific product pricing**: Set different prices per country directly in the Adapty dashboard — Adapty syncs changes to App Store Connect and Google Play automatically. Every pricing update is logged in the audit log, so no change goes untracked. [Learn more](edit-product) - **Country-level competitor pricing in Autopilot**: Compare your subscription prices against competitors in your top markets. [Learn more](autopilot#competitor-analysis) - **Onboarding version control**: Track and manage versions of your onboardings with a full version history. Review changes and roll back when needed. [Learn more](onboarding-version-control) - **Paywall conversion charts in analytics**: Two new conversion charts — Paywall view → Trial and Paywall view → Paid — show how your paywalls convert viewers into subscribers. [Learn more](analytics-conversion) - **Duplicate segments**: Copy an existing segment with all its filters instead of rebuilding a similar one from scratch. Useful when running multiple campaigns or A/B tests with overlapping audiences. [Learn more](segments#duplicate-segments) - **Push notifications in Adapty mobile app**: Configure push notifications for 14 event types directly in the Adapty iOS app to stay on top of subscription activity without opening the dashboard. [Learn more](push-notifications) - **Kotlin Multiplatform SDK 3.15**: Adds onboarding support, web paywalls, and API improvements. [Learn more](migration-to-kmp-315) - **Capacitor SDK 3.16**: Adds Capacitor 8 support. Projects using Capacitor 7 should stay on SDK v3.15. [Learn more](migration-to-capacitor-316) - **LLM-assisted SDK integration guides**: Step-by-step guides for integrating Adapty with the help of AI coding assistants. Each guide walks your LLM through the full implementation, from dashboard setup to purchases. [iOS](adapty-cursor) | [Android](adapty-cursor-android) | [React Native](adapty-cursor-react-native) | [Flutter](adapty-cursor-flutter) | [Unity](adapty-cursor-unity) | [Kotlin Multiplatform](adapty-cursor-kmp) | [Capacitor](adapty-cursor-capacitor) ## January 2026 - **Capacitor SDK officially released**: The Capacitor SDK is now production-ready after extensive testing. Build subscription apps for iOS and Android using Capacitor with full Adapty integration support. [Learn more](capacitor-sdk-overview) - **Autopilot for new apps**: Autopilot analysis is now available even if your app doesn't have extensive transaction history yet. Get data-driven price optimization recommendations and create your growth plan from day one. [Learn more](autopilot) - **Global pricing opportunities in Autopilot**: Identify revenue potential across your top-performing markets with country-specific pricing recommendations. Autopilot analyzes conversion rates and purchasing power for your next top 5 countries, providing data-driven insights on whether to increase, decrease, or maintain prices based on the Adapty Pricing Index. [Learn more](autopilot) - **Billing recovery conversion metrics**: New analytics charts track revenue recovered from billing issues and grace periods. Monitor "Billing issue converted", "Billing issue converted revenue", "Grace period converted", and "Grace period converted revenue" to measure your retention recovery efforts. - **Direct ad management in Apple Ads Manager**: Create and manage your Apple Ads campaigns directly within Adapty without switching between platforms. [Learn more](ads-manager-manage-ads) - **Apple Ads Manager analytics**: Access detailed ad-level performance metrics and attribution data within Adapty. View campaign performance, ad group analytics, and attribution insights in a unified dashboard. [Learn more](adapty-ads-manager-analytics) - **Apple Ads attribution charts**: Combine multiple attribution metrics in customizable charts to analyze your Apple Ads performance alongside subscription data. [Learn more](adapty-ads-manager-analytics#charts) - **Apple Ads attribution segments**: Create user segments based on Apple Ads attribution data with a streamlined two-click workflow. Target users by campaign, ad group, or keyword for more precise analysis and experiments. [Learn more](ads-manager-create-segments) - **New documentation platform**: The documentation site has been migrated to a new platform, enabling faster feature updates and improved user experience with enhanced search, navigation, and content organization. ## December 2025 - **Apple Ads Manager documentation**: Combine your Apple Search Ads campaign data with revenue metrics in a single analytics dashboard. The new documentation covers campaign creation, ad group management, and ways to track your ad spend ROI alongside subscription performance. [Learn more](ads-manager) - **In-app web paywalls**: Display web-based paywalls within your app using an in-app browser, providing a seamless experience without external redirects. [iOS](ios-web-paywall#open-web-paywalls-in-an-in-app-browser) | [Android](android-web-paywall#open-web-paywalls-in-an-in-app-browser) | [React Native](react-native-web-paywall#open-web-paywalls-in-an-in-app-browser) | [Flutter](flutter-web-paywall#open-web-paywalls-in-an-in-app-browser) - **Line-by-line AI translation in Paywall Builder**: Select specific text elements in your paywall and translate them with AI assistance, making localization more precise and flexible. [Learn more](add-paywall-locale-in-adapty-paywall-builder#translating-paywalls-with-ai) - **Rolling segments**: Create dynamic audience segments that automatically update based on moving time windows. For example, create a segment for "users who installed the app in the last 7 days" that continuously refreshes to always show your newest customers. [Learn more](segments#available-attributes) - **Meta and TikTok campaign setup guides**: Step-by-step documentation for creating and tracking campaigns on Meta (Facebook & Instagram) and TikTok, with conversion tracking and analytics integration. [Meta](meta-create-campaign) | [TikTok](tiktok-create-campaign) - **Manual paywall implementation quickstart guides**: Implement in-app purchases faster with step-by-step quickstart guides that show you how to integrate Adapty SDK into your custom paywall UI. [iOS](ios-implement-paywalls-manually) | [Android](android-implement-paywalls-manually) | [React Native](react-native-implement-paywalls-manually) | [Flutter](flutter-implement-paywalls-manually) | [Unity](unity-implement-paywalls-manually) | [Kotlin Multiplatform](kmp-quickstart-manual.md) | [Capacitor](capacitor-quickstart-manual.md) - **In-app browser for onboarding links**: External links in onboardings now open in an in-app browser by default, keeping users in your app. You can customize this behavior to use external browsers if needed. [iOS](ios-present-onboardings#customize-how-links-open-in-onboardings) | [Android](android-present-onboardings#customize-how-links-open-in-onboardings) | [React Native](react-native-present-onboardings#customize-how-links-open-in-onboardings) - **Improved Autopilot suggestions**: Autopilot now provides better price optimization recommendations based on enhanced analysis of your subscription data. [Try Autopilot](autopilot) - **Documentation dark mode**: The docs now support dark mode with automatic system preference detection or manual toggle at the top right. --- # File: generate-in-app-purchase-key --- --- title: "Generate In-App Purchase Key in App Store Connect" description: "Generate an in-app purchase key for secure transactions." --- The **In-App Purchase Key** is a specialized API key created within App Store Connect to validate the purchases by confirming their authenticity. :::note To generate API keys for the App Store Server API, you must hold either an Admin role or an Account Holder role in App Store Connect. You can also read about how to generate API Keys in the [Apple Developer Documentation](https://developer.apple.com/documentation/appstoreserverapi/creating-api-keys-to-authorize-api-requests). ::: 1. Open **App Store Connect**. Proceed to [**Users and Access** → **Integrations** → **In-App Purchase**](https://appstoreconnect.apple.com/access/integrations/api/subs) section. 2. Then click the add button **(+)** next to the **Active** title.
3. In the opened **Generate In-App Purchase Key** window, enter the name of the key for your future reference. It will not be used in Adapty.
4. Click the **Generate** button. Once the **Generate in-App Purchase Key** window closes, you'll see the created key in the **Active** list.
5. Once you've generated your API key, click the **Download In-App Purchase Key** button to obtain the key as a file.
6. In the **Download in-App Purchase Key** window, click the **Download** button. The file is saved to your computer.
It's crucial to keep this file secure for future uploading to the Adapty Dashboard. Note that the generated file can only be downloaded once, so ensure safe storage until you upload it. The generated .p8 key from the **In-App Purchase section** will be used when [configuring the initial integration of Adapty with the App Store](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file).
**What's next:**
- [Configure App Store integration](app-store-connection-configuration)
---
# File: app-store-connection-configuration
---
---
title: "Configure App Store integration"
description: "Configure your App Store connection for seamless subscription tracking."
---
3. Copy **Issuer ID** and paste it to the **In-app purchase Issuer ID** field in the Adapty Dashboard.
4. Copy the **Key ID** and paste it to the **In-app purchase Key ID** field in the Adapty Dashboard.
## Step 3. Upload In-App Purchase Key file
Upload the **In-App Purchase Key** file you've downloaded in the [Generate In-App Purchase Key in App Store Connect](generate-in-app-purchase-key) section
into the **Private key (.p8 file)** field in the Adapty Dashboard.
## Step 4. For trials and special offers – set up promotional offers
:::important
This step is required if your app has [trials or other promotional offers](offers).
:::
1. Copy the same key ID you used in [Step 2](#step-2-provide-issuer-id-and-key-id) to the **Subscription key ID** field in the **App Store promotional offers** section.
2. Upload the same **In-App Purchase Key** file you used in [Step 3](#step-3-upload-in-app-purchase-key-file) to the **Subscription key (.p8 file)** area in the **App Store promotional offers** section.
## Step 5. Enter App Store shared secret
The **App Store shared secret**, also known as the App Store Connect Shared Secret, is a 32-character hexadecimal string used for in-app purchases and subscription receipt validation.
1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section.
2. Scroll down to the **App-Specific Shared Secret** sub-section.
:::info
If the **App-Specific Shared Secret** sub-section is absent, make sure you have an Account Holder or Admin role. If you have an Admin role and yet cannot see the **App-Specific Shared Secret** sub-section, ask the Account Holder of the app (the person who has created the application in the App Store Connect) to generate the App Store shared secret for the app. After that, the sub-section will be shown to Admins as well.
:::
3. Click the **Manage** button.
4. In the opened **App-Specific Shared Secret** window, copy the **Shared Secret**. If no shared secret is visible, first click either the **Manage** or **Generate** button whichever is available, and then copy the **Shared Secret**.
5. Paste the copied **Shared Secret** to the **App Store shared secret** field in the Adapty Dashboard.
6. Click the **Save** button in the Adapty Dashboard to confirm the changes.
## Step 6. Add App Store Connect API key
Generate an App Store Connect API key and add it to Adapty to be able to [manage your products in the App Store from the Adapty dashboard](create-product#create-product-and-push-to-store):
1. In App Store Connect, go to [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api) and click **+**.
2. In the **Generate API key window**, enter a name for the key and grant it the **Admin** access.
3. Click **Download** next to your key. Note that you can download it only once.
4. In the Adapty dashboard, go to [**App settings > iOS SDK**](https://app.adapty.io/settings/ios-sdk) and click **Connect API key**.
5. Fill in the fields in the window:
- **Issuer ID**: Copy from [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api). It is above the **API keys** table.
- **Key ID**: Copy from [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api). It is in the **API keys** table next to your key.
- **API key**: Upload the API key file you've downloaded from App Store Connect.
6. Click **Connect**.
**What's next**
- [Enable App Store server notifications](enable-app-store-server-notifications)
---
# File: enable-app-store-server-notifications
---
---
title: "Enable App Store server notifications"
description: "Enable App Store server notifications to track subscription events in real time."
---
Setting up App Store server notifications is crucial for ensuring data accuracy as it enables you to receive updates instantly from the App Store, including information on refunds and other events.
1. Copy the **URL for App Store server notification** in the Adapty Dashboard.
2. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section, **App Store Server Notifications** subsection.
3. Paste the copied **URL for App Store server notification** into the **Production Server URL** and **Sandbox Server URL** fields.
## Raw events forwarding
Sometimes, you might still want to receive raw S2S events from Apple. To continue receiving them while using Adapty, just add your endpoint to the **URL for forwarding raw Apple events** field, and we'll send raw events as-is from Apple.
**What's next**
Set up the Adapty SDK for:
- [iOS](sdk-installation-ios)
- [React Native](sdk-installation-reactnative)
- [Flutter](sdk-installation-flutter)
- [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md)
- [Unity](sdk-installation-unity)
---
# File: enabling-of-devepoler-api
---
---
title: "Enable Developer APIs in Google Play Console"
description: "Enable Adapty's Developer API to automate and streamline subscription management in your app."
---
If your mobile app is available in the Play Store, activating Developer APIs is crucial for integrating it with Adapty. This step ensures seamless communication between your app and our platform, facilitating automated processes and real-time data analysis to optimize your subscription model. The following APIs should be enabled:
- [Google Play Android Developer API](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com)
- [Google Play Developer Reporting API](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com)
- [Cloud Pub/Sub API](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com)
If your app isn't distributed via the Play Store, you can skip this step. However, if you do sell through the Play Store, you can delay this step for now, though it's crucial for Adapty's basic functionality. After completing the onboarding process, you can configure the application store settings in the **App settings** section.
Here's how to enable Developer APIs in the Google Play Console:
1. Open the [Google Cloud Console](https://console.cloud.google.com/).
2. In the top-left corner of the Google Cloud window, select the project you wish to use or create a new one. Ensure you use the same Google Cloud project until you upload the service account key file to Adapty.
3. Open the [**Google Play Android Developer API**](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com) page.
4. Click the **Enable** button and wait for the status **Enabled** to show. This means the Google Android Developer API is enabled.
5. Open the [**Google Play Developer Reporting API**](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com) page.
6. Click the **Enable** button and wait for the status **Enabled** to show.
7. Open the [**Cloud Pub/Sub API**](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com) page.
8. Click the **Enable** button and wait for the status **Enabled** to show.
Developer APIs are enabled.
You can recheck it on the [**APIs & Services**](https://console.cloud.google.com/apis/dashboard) page of the Google Cloud Console. Scroll the page down, and validate the table at the bottom of the page contains all 3 APIs:
- Google Play Android Developer API
- Google Play Developer Reporting API
- Cloud Pub/Sub API
**What's next**
- [Create a service account in the Google Cloud Console](create-service-account)
---
# File: create-service-account
---
---
title: "Create service account in the Google Cloud Console"
description: "Learn how to create a service account for secure API access in Adapty."
---
For Adapty to automate data access, a service account is necessary in the Google Play Console.
1. Open [**IAM & Admin** - > **Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) section of the Google Cloud Console. Make sure you use the correct project.
2. In the **Service accounts** window, click the **Create service account** button.
3. In the **Service account details** sub-section of the **Create service account** window, enter the **Service Account Name** you want. We recommend including "Adapty" in the name to indicate the purpose of this account. The **Service account ID** will be created automatically.
4. Copy the service account email address and save it for future usage.
5. Click the **Create and continue** button.
6. In the **Select a role** drop-down list of the **Grant this service account access to project** sub-section, select **Pub/Sub -> Pub/Sub Admin**. This role is required to enable real-time developer notifications.
7. Click the **Add another role** button.
8. In a new **Role** drop-down list, select **Monitoring -> Monitoring Viewer**. This role is required to allow monitoring of the notification queue.
9. Click the **Continue** button.
10. Click the **Done** button without any changes. The **Service accounts** window opens.
**What's next**
- [Grant permissions to the service account in the Google Play Console](grant-permissions-to-service-account)
---
# File: grant-permissions-to-service-account
---
---
title: "Grant permissions to service account in the Google Play Console"
description: "Grant permissions to service accounts for secure and efficient API access."
---
Grant the required permissions to the service account that Adapty will use to manage subscriptions and validate purchases.
1. Open the [**Users and permissions**](https://play.google.com/console/u/0/developers/8970033217728091060/users-and-permissions) page in the Google Play Console and click the **Invite new users** button.
2. In the **Invite user** page, enter the email of the service users you've created.
3. Switch to the **Account permissions** tab.
4. Select the following permissions:
- View app information and download bulk reports (read-only)
- View financial data, orders, and cancellation survey responses
- Manage orders and subscriptions
- Manage store presence
5. Click the **Invite user** button.
6. In the **Send invite?** window, click the **Send invite** button. The service account will show in the user list.
**What's next**
- [Generate the service account key file in the Google Play Console](create-service-account-key-file)
---
# File: create-service-account-key-file
---
---
title: "Generate service account key file in the Google Play Console"
description: "Learn how to create a service account key file for seamless integration with Adapty."
---
To link your mobile app on the Play Store with Adapty, you'll need to generate special service account key files in the Google Play Console and upload them to Adapty. These files help secure your app and prevent unauthorized access.
:::warning
It usually takes at least 24 hours for your new service account to become active. However, there's a [hack](https://stackoverflow.com/a/60691844). After creating the service account in the [Google Play Console](https://play.google.com/apps/publish/), open any application and navigate to **Monetize** -> **Products** -> **Subscriptions/In-app products**. Edit the description of any product and save the changes. This should activate the service account immediately, and you can revert the changes afterward.
:::
1. Open the [**Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) section in the Google Play Console. Ensure you’ve selected the correct project.
2. In the window that opens, click **Add key** and choose **Create new key** from the dropdown menu.
3. In the **Create private key for [Your_project_name]** window, click **Create**. Your private key will be saved to your computer as a JSON file. You can find it using the file name provided in the **Private key saved to your computer** window.
4. In the **Create private key for Your_project_name** window, click the **Create** button. This action will save your private key on your computer as a JSON file. You can use the name of the file provided in the opened **Private key saved to your computer** window to locate it if needed.
You’ll need this file when you [configure Google Play Store integration](google-play-store-connection-configuration).
:::warning
It usually takes at least 24 hours for your new service account to become active. However, there's a [hack](https://stackoverflow.com/a/60691844). After creating the service account in the [Google Play Console](https://play.google.com/apps/publish/), open any application and navigate to **Monetize** -> **Products** -> **Subscriptions/In-app products**. Edit the description of any product and save the changes. This should activate the service account immediately, and you can revert the changes afterward.
:::
**What's next**
- [Configure Google Play Store integration](google-play-store-connection-configuration)
---
# File: google-play-store-connection-configuration
---
---
title: "Configure Google Play Store integration"
description: "Configure Google Play Store connection in Adapty for smooth in-app purchase handling."
---
This section outlines the integration process for your mobile app sold via Google Play with Adapty. You'll need to input your app's configuration data from the Play Store into the Adapty Dashboard. This step is crucial for validating purchases and receiving subscription updates from the Play Store within Adapty.
You can complete this process during the initial onboarding or make changes later in the **App Settings** of the Adapty Dashboard.
:::danger
Configuration change is only acceptable until you release your mobile app with integrated Adapty paywalls. The change after the release will break the integration and the paywalls will stop showing in your mobile app.
:::
## Step 1. Provide Package name
The Package name is the unique identifier of your app in the Google Play Store. This is required for the basic functionality of Adapty, such as subscription processing.
1. Open the [Google Play Developer Console](https://play.google.com/console/u/0/developers).
2. Select the app whose ID you need. The **Dashboard** window opens.
3. Find the product ID under the application name and copy it.
4. Open the [**App settings**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu.
5. In the **Android SDK** tab of the **App settings** window, paste the copied **Package name**.
## Step 2. Upload the account key file
1. Upload the service account private key file in JSON format that you have created at the [Create service account key file](create-service-account) step into the **Service account key file** area.
Don't forget to click the **Save** button to confirm the changes.
**What's next**
- [Enable Real-time developer notifications (RTDN) in the Google Play Console](enable-real-time-developer-notifications-rtdn)
---
# File: enable-real-time-developer-notifications-rtdn
---
---
title: "Enable Real-time developer notifications (RTDN) in Google Play Console"
description: "Stay informed about critical events and maintain data accuracy by enabling Real-time Developer Notifications (RTDN) in the Google Play Console for Adapty. Learn how to set up RTDN to receive instant updates about refunds and other important events from the Play Store"
---
Setting up real-time developer notifications (RTDN) is crucial for ensuring data accuracy as it enables you to receive updates instantly from the Play Store, including information on refunds and other events.
## Enable notifications
1. Ensure you have **Google Cloud Pub/Sub** enabled. Open [this link](https://console.cloud.google.com/flows/enableapi?apiid=pubsub) and select your app project. If you haven't enabled **Google Cloud Pub/Sub**, you must do it here.
2. Go to [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu and copy the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title.
:::note If the contents of the **Enable Pub/Sub API** field have a wrong format (the correct format starts with `projects/...`), refer to the [Fixing incorrect format in Enable Pub/Sub API field](enable-real-time-developer-notifications-rtdn#fixing-incorrect-format-in-enable-pubsub-api-field) section for help. ::: 3. Open the [Google Play Console](https://play.google.com/console/), choose your app, and go to **Monetize with Play** -> **Monetization setup**. In the **Google Play Billing** section, select the **Enable real-time notifications** check-box. 4. Paste the contents of the **Enable Pub/Sub API** field you've copied in the Adapty **App Settings** into the **Topic name** field. 5. Click **Save changes** in the Google Play Console.
## Test notifications
To check whether you have successfully subscribed to real-time developer notifications:
1. Save changes in the Google Play Console settings.
2. Under the **Topic name** in Google Play Console, click **Send test notification**.
3. Go to the [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) in Adapty. If a test notification has been sent, you'll see its status above the topic name.
## Fixing incorrect format in Enable Pub/Sub API field
If the contents of the **Enable Pub/Sub API** field are in the wrong format (the correct format starts with `projects/...`), follow these steps to troubleshoot and resolve the issue:
### 1. Verify API Enablement and Permissions
Carefully ensure that all required APIs are enabled, and permissions are correctly granted to the service account. Even if you've already completed these steps, it’s important to go through them again to make sure no sub-step was missed. Repeat the steps in the following sections:
1. [Enable Developer APIs in Google Play Console](enabling-of-devepoler-api)
2. [Create service account in the Google Cloud Console](create-service-account)
3. [Grant permissions to service account in the Google Play Console](grant-permissions-to-service-account)
4. [Generate service account key file in the Google Play Console](create-service-account-key-file)
5. [Configure Google Play Store integration](google-play-store-connection-configuration)
### 2. Adjust Domain Policies
Change the **Domain restricted contacts** and **Domain restricted sharing** policies:
1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select the project where you created the service account to manage your app.
2. In the **Quick Access** section, choose **IAM & Admin**.
3. In the left pane, choose **Organization Policies**.
4. Find the **Domain restricted contacts** policy.
5. Click the ellipsis button in the **Actions** column and choose **Edit policy**.
6. In the policy editing window:
1. Under **Policy source**, select the **Override parent's policy** radio-button.
2. Under **Policy enforcement**, select the **Replace** radio button.
3. Under **Rules**, click the **ADD A RULE** button.
4. Under **New rule** -> **Policy values**, choose **Allow All**.
5. Click **SET POLICY**.
7. Repeat steps 4-6 for the **Domain restricted sharing** policy.
Finally, recreate the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title. The field will now have the correct format.
Make sure to switch the **Policy source** back to **Inherit parent's policy** for the updated policies once you've successfully enabled Real-time Developer Notifications (RTDN).
## Raw events forwarding
Sometimes, you might still want to receive raw S2S events from Google. To continue receiving them while using Adapty, just add your endpoint to the **URL for forwarding raw Google events** field, and we'll send raw events as-is from Google.
---
**What's next**
Set up the Adapty SDK for:
- [Android](sdk-installation-android)
- [React Native](sdk-installation-reactnative)
- [Flutter](sdk-installation-flutter)
- [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md)
- [Unity](sdk-installation-unity)
---
# File: stripe
---
---
title: "Initial integration with Stripe"
description: "Integrate Stripe with Adapty for seamless subscription payment processing."
---
Adapty supports web2app subscription flows by tracking web payments and subscriptions made through [Stripe](https://stripe.com/).
This integration covers web-initiated purchases (Stripe Checkout, hosted payment pages, or custom web flows) and synchronizes them with mobile app access and analytics.
It is useful in the following scenarios:
- Automatically providing access to paid features for users who purchased on the web but later installed the app and logged in to their account
- Having all the subscription analytics in a single Adapty Dashboard (including cohorts, predictions, and the rest of our analytics toolbox)
Even though web purchases are becoming increasingly popular for apps, the Apple App Store allows a different system than in-app purchases for digital goods only in the USA. Ensure you don't promote your web subscriptions inside your app for other countries. Otherwise, your app may get rejected or banned.
The steps below outline how to configure the Stripe integration.
:::important
This integration focuses on tracking and syncing Stripe web purchases. If you need to send users from the app to a web checkout, see [Web paywalls](web-paywall.md).
:::
### 1\. Connect Stripe to Adapty
This integration mainly relies on Adapty pulling subscription data from Stripe via the webhook. Therefore, you need to connect your Adapty account to your Stripe account by providing API Keys and using Adapty's webhook URL in Stripe. To automate configuring your webhook, install the Adapty app in Stripe:
:::note
The steps below are the same for Stripe's Production and Test modes, but you will need to use different API keys for each.
:::
0. Determine if you are connecting Stripe in test mode or live mode. If you are initially doing this in test mode, you will need to repeat the steps below for live mode again.
1. Go to the [Stripe App Marketplace](https://marketplace.stripe.com/apps/adapty) and install the Adapty app. Note that the sandbox mode doesn't support installing apps. You can only do it in the production or test mode.
2. Give the app the required permissions. This will allow Adapty to access the subscription data and history. Then, click **Continue to app settings** to proceed.
At the bottom of the permission pop-up, you can select whether to install the app in live or test mode.
3. In the pop-up, generate a new restricted key. You will need to verify your identity using your email, Touch ID, or security key. Once you generate a key, you won't be able to see it again, so store it securely in a password manager or a secret store.
4. Copy the generated key from the pop-up and go to Adapty's [App Settings → Stripe](https://app.adapty.io/settings/stripe). Paste the key in the **Stripe App Restricted API Key** section depending on your mode. Note that you must generate different keys for test and live modes.
You're all set! Next, create your products on Stripe and add them to Adapty.
2. Click the **Reveal live (test) key button** next to the **Secret key** title, then copy it and go to Adapty's [App Settings → Stripe](https://app.adapty.io/settings/stripe). Paste the key here:
3. Next, copy the Webhook URL from the bottom of the same page in Adapty. Go to [**Developers** → **Webhooks**](https://dashboard.stripe.com/webhooks) in Stripe and click the **Add endpoint** button:
4. Paste the webhook URL from Adapty into the **Endpoint URL** field. Then choose the **Latest API version** in the webhook **Version** field. Then select the following events:
- charge.refunded
- customer.subscription.created
- customer.subscription.deleted
- customer.subscription.paused
- customer.subscription.resumed
- customer.subscription.updated
- invoice.created
- invoice.updated
- payment_intent.succeeded
5. Press "Add endpoint" and then press "Reveal" under the "Signing secret". This is the key that is used to decode the webhook data on the Adapty's side, copy it after revealing:
6. Finally, paste this key into Adapty's App Settings → Stripe under "Stripe Webhook Secret":
:::warning
At the moment Adapty only supports **Flat rate** ($9.99/month) or **Package pricing** ($9.99/10 units), as those behave similar to app stores. **Tiered pricing**, **Usage-based fee** and **Customer chooses price** options are not supported
:::
### 3\. Add Stripe products to Adapty
:::warning
Products are required! Be sure to create your Stripe products in the Adapty Dashboard. Adapty only tracks events for transactions linked to these products, so don’t skip this step—otherwise, transaction events won’t be created.
:::
We treat Stripe the same way as App Store and Google Play: it is just another store where you sell your digital products. So it is configured similarly: simply add Stripe products (namely their `product_id` and `price_id`) to the Products section of Adapty:
Product IDs in Stripe look like `prod_...` and price IDs look like `price_...`. They are pretty easy to find for each product in Stripe's [Product Catalog](https://dashboard.stripe.com/products?active=true), once you open any Product:
After you've added all the necessary products, the next step is to let Stripe know about which user is making the purchase, so it could get picked up by Adapty!
### 4\. Enrich purchases made on the web with your user ID
Adapty relies on the webhooks from Stripe to provide and update access levels for users as the only source of information. But you have to provide additional info from your end when working with Stripe for this integration to work properly.
For access levels to be consistent across platforms (web or mobile), you have to make sure that there is a single user ID to rely on which Adapty can recognize from the webhooks as well. This could be user's email, phone number or any other ID from the authorization system you're utilizing.
Figure out which ID you would like to use to identify your users. Then, access the part of your code that's initiatilizing the payment through Stripe — and add this user ID to the `metadata` object of either [Stripe Subscription](https://stripe.com/docs/api/subscriptions/object#subscription_object-metadata) (`sub_...`) or [Checkout Session](https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-metadata) object (`ses_...`) as `customer_user_id` like so:
```json showLineNumbers title="Stripe Metadata contents"
{'customer_user_id': "YOUR_USER_ID"}
```
This one simple addition is the only thing that you have to do in your code. After that, Adapty will parse all the webhooks it receives from Stripe, extract this `metadata` and correctly associate subscriptions with your customers.
:::warning
User ID is required
Otherwise, we have no way to match this user and provide him the access level on the mobile.
If you don't supply `customer_user_id` to the `metadata`, you will have the option to make Adapty look for `customer_user_id` in other places: either `email` from Stripe's Customer object or `client_reference_id` from Stripe's Session.
Learn more about configuring profile creation behavior [below](stripe#profile-creation-behavior)
:::
:::note
Customer in Stripe is also required
If you are using Checkout Sessions, [make sure you're creating a Stripe Customer](https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-customer_creation) by setting `customer_creation` to `always`.
:::
### 5\. Provide access to users on the mobile
To make sure your mobile users arriving from web can access the paid features, just call `Adapty.activate()` or `Adapty.identify()` with the same `customer_user_id` you've provided on the previous step (see
2. Give the key a name and set the expiration date. For the API key to work with Adapty, you need to grant it the **Read** permission for all entities. Click **Save**.
3. Click **Copy key**.
4. In Adapty, go to [App Settings → Paddle](https://app.adapty.io/settings/paddle) and paste the key in the **Paddle API key** section.
:::warning
If you set an expiration date for your Paddle API key, you must manually generate a new key and update it in Adapty before expiration. The integration will stop working without warning when the key expires, and users won't be able to make purchases.
:::
### 1.2. Add events that will be sent to Adapty
1. Copy the **Webhook URL** from the same **Paddle** page in Adapty.
2. In Paddle, go to [**Developer Tools → Notifications**](https://vendors.paddle.com/notifications-v2) and click **New destination** to add a webhook.
3. Enter a descriptive name for the webhook. We recommend including "Adapty" in it, so you can easily find it when needed.
4. Paste the **Webhook URL** from Adapty into the **URL** field. Ensure you are using the webhook for the right environment.
5. Set **Notification type** to **Webhook**.
6. Select the following events:
- `subscription.created`
- `subscription.updated`
- `transaction.created`
- `transaction.updated`
- `adjustment.created`
- `adjustment.updated`
7. Click **Save destination** to finalize the webhook setup.
### 1.3. Retrieve and add the webhook secret key
1. In the **Notifications** window, click the three dots next to the webhook you just created and select **Edit destination**.
2. A new field called the **Secret key** will appear in the **Edit destination** panel. Copy it.
3. In Adapty, go to [App Settings → Paddle](https://app.adapty.io/settings/paddle) and paste the key into the **Notification secret key** field. This key is used to verify webhook data in Adapty.
### 1.4. Match Paddle customers with Adapty profiles
Adapty needs to link each purchase to a [customer profile](profiles-crm) so it can be used in your app. By default, profiles are created automatically when Adapty receives webhooks from Paddle. You can choose which value to use as the `customer_user_id` in Adapty:
1. **Default and recommended:** The `customer_user_id` you pass in the `custom_data` field (see [Paddle docs](https://developer.paddle.com/build/transactions/custom-data))
2. The `email` from the Paddle Customer object (see [Paddle docs](https://developer.paddle.com/paddlejs/methods/paddle-checkout-open#params))
3. The Paddle Customer ID in the `ctm-...` format (see [Paddle docs](https://developer.paddle.com/paddlejs/methods/paddle-checkout-open#params))
4. Don't create profiles. Choose this option if you want to have more control over your customer profiles and handle it yourself.
You can configure which value to use in the **Profile creation behavior** field in [App Settings → Paddle](https://app.adapty.io/settings/paddle).
## 2. Add Paddle products to Adapty
:::warning
Be sure to add your Paddle products to the Adapty Dashboard or add a Paddle product ID to your existing products. Adapty only tracks events for transactions tied to these products. If you skip this step, transaction events won't be created.
:::
Paddle works in Adapty just like App Store and Google Play — it's another platform where you sell digital products. To configure it, add the relevant `product_id` and `price_id` values from Paddle in the [Products](https://app.adapty.io/products) section in Adapty.
In Paddle, product IDs look like `pro_...` and price IDs like `pri_...`. You'll find them in your [Paddle product catalog](https://vendors.paddle.com/products-v2) once you open a specific product:
Once your products are added, the next step is ensuring Adapty can link the purchase to the right user.
## 3\. Provide access to users on the mobile
To ensure users who buy on the web get access on mobile, call `Adapty.activate()` or `Adapty.identify()` using the same `customer_user_id` you passed when the purchase was made. See [Identifying users](identifying-users) for details.
## 4\. Test your integration
Once everything's set up, you can test your integration. Transactions made in Paddle's Test environment will appear as **Test** in Adapty. Transactions from the Production environment will appear as **Production**.
Your integration is now complete. Users can purchase subscriptions on your website and automatically access premium features in your mobile app, while you track all subscription analytics from your unified Adapty dashboard.
## Important considerations
- In Adapty's analytics, transaction amounts include taxes and Paddle fees, which differs from Paddle's dashboard where amounts are shown after taxes and fees. This means the numbers you see in Adapty will be higher than those in your Paddle dashboard.
- Unlike other stores, refunds in Paddle only affect the specific transaction being refunded and do not automatically cancel the subscription. The subscription will continue to be active unless explicitly canceled.
- You can also include `variation_id` in the `custom_data` field to attribute purchases to specific paywall instances. Adapty will process this data from webhooks and include it in analytics.
### Paid trials
When working with paid trials in Paddle, you need to create two products in Adapty:
1. Create a non-subscription product and link it to the Paddle price that charges for the trial period.
2. Then create a subscription product (Monthly/Weekly/etc.) and link it to the Paddle price that has the free trial component.
From Paddle's perspective, this is one product with two prices in a single transaction - one price for the trial charge (e.g., $0.99) and another price for the free trial ($0.00).
From Adapty's perspective, this creates two separate events: a non-subscription purchase for the trial payment and a trial started event for the subscription product.
For example, when a user starts a $0.99 paid trial for a $9.99/month subscription, Paddle creates one transaction with both prices, while Adapty processes this as a non-subscription purchase of $0.99 (immediate payment) and a trial started event at $0.00 (future subscription at $9.99/month).
:::note
When users cancel a paid trial, you get the **Trial expired** and **Trial renewal canceled** events.
:::
## Get more from your Paddle data
:::important
For your Paddle events to work with integrations, your users must be logged into the app using their App Store/Google Play account at least once.
:::
Once you integrate with Paddle, Adapty is ready to provide insights right away. To make the most of your Paddle data, you can set up additional Adapty integrations to forward Paddle events—bringing all your subscription analytics into a single Adapty Dashboard.
Integrations you can use to forward and analyze your Paddle events:
- [AppsFlyer](appsflyer.md)
- [Webhook](https://adapty.io/docs/webhook)
- [Posthog](https://adapty.io/docs/posthog)
## Current limitations
- **Cancellations**: Paddle has two subscription cancellation options:
1. Immediate cancellation: The subscription is canceled immediately.
2. Cancellation at the end of the period: The subscription cancels at the end of the current billing period (similar to in-app subscriptions on the app stores).
- **Refunds**: Adapty tracks full and partial refunds.
- **Grace period**: By default, Paddle applies a fixed 30-day grace period for billing issues, during which the subscription remains active. You can [customize the grace period duration and the action after its end (pausing or canceling the subscription)](https://developer.paddle.com/build/retain/configure-payment-recovery-dunning#prerequisites).
**Trials**: If payment collection fails after a trial ends, the subscription status changes to `past_due`. In production, Paddle's Retain applies a dunning window to attempt payment recovery before the subscription is canceled or paused. In sandbox, Retain is unavailable, so no payment retries are attempted and the subscription remains `past_due` indefinitely.
---
**See also:**
- [Validate purchase in Paddle, get access level, and import transaction history from Paddle with server-side API](api-adapty/operations/validatePaddlePurchase)
---
# File: custom-store
---
---
title: "Initial integration with other stores"
description: "Adapty Initial Integration with App Store: A Quick Guide"
---
We're thrilled to have you on board with Adapty! Our priority is to help you hit the ground running and achieve the best possible outcomes for your app.
The initial integration is only needed for [App Store](initial_ios), [Google Play](initial-android), [Stripe](stripe), and [Paddle](paddle.md) since Adapty verifies your apps, products, and offers with these stores.
Adapty doesn’t validate data with other app stores and does not process purchases made through them. However, you can still mark products sold through other stores for Adapty to grant access to paid content after a successful purchase, reflect transactions in your analytics, and share them via integrations.
:::important Make sure your backend processes the purchase and sends the transaction to Adapty using the [Adapty server-side API](getting-started-with-server-side-api). Adapty will only provide access, trigger a transaction event, send it to integrations, and reflect it in analytics after the transaction is received. ::: To mark a product as sold via a custom app store, select the app store when creating a product. If the store you need isn’t listed, here’s how to create one: 1. On the **Products** page, open the product you want to sell through a custom app store. 2. Choose the app store you want to sell through. If it’s not listed, click the **Create Custom Store** button.
3. Enter the store’s **Title** and **Store ID**.
4. Click the **Create store** button.
If your backend is set up correctly, Adapty will receive product transactions from this custom store, reflect them in analytics, the [**Event Feed**](event-feed), and [integrations](https://app.adapty.io/integrations), and grant access accordingly.
## Get more from your custom store data
:::important
For your custom store events to work with integrations, your users must be logged into the app using their App Store/Google Play account at least once.
:::
Once you set up your custom store integration, Adapty is ready to provide insights right away. To make the most of your data, you can set up additional Adapty integrations to forward custom store events—bringing all your subscription analytics into a single Adapty Dashboard.
Integrations you can use to forward and analyze your custom store events:
- [AppsFlyer](appsflyer.md)
- [Webhook](https://adapty.io/docs/webhook)
- [Posthog](https://adapty.io/docs/posthog)
---
# File: transfer-apps
---
---
title: "Transfer your app to a different account"
description: "Change the app owner in Adapty "
---
Transfer your app to a different owner when your company is acquired, you're selling your app, or reorganizing business entities. The transfer process involves coordinating changes in Adapty, App Store Connect, and Google Play Console to ensure continuous service.
## Transfer app ownership
Complete the store transfer first, then transfer the app in Adapty. This order ensures purchases continue working throughout the transition.
:::note
Do not delete or recreate products during the transfer process. Do not change product IDs until after verifying the transfer completed successfully.
:::
### App Store (iOS) transfer
:::important
App Store Connect API keys (Issuer ID, Key ID, .p8 file) are account-scoped, not app-scoped. After transfer, you must generate new API keys from the new owner's account and update them in Adapty.
:::
1. **New owner:** Create an Adapty account at [app.adapty.io](https://app.adapty.io) if you don't have one.
2. **Old owner:** Initiate the app transfer in App Store Connect following Apple's [transfer guide](https://developer.apple.com/help/app-store-connect/transfer-an-app/overview-of-app-transfer).
3. **New owner:** Accept the transfer in App Store Connect.
4. **Old owner:** Email [support@adapty.io](mailto:support@adapty.io) to transfer the app in Adapty. Provide the app name and new owner's email address.
5. **New owner:** After receiving the app in Adapty, complete the [App Store integration guide](initial_ios) to generate and configure all credentials under your account.
### Google Play (Android) transfer
1. **New owner:** Create an Adapty account at [app.adapty.io](https://app.adapty.io) if you don't have one.
2. **Both owners:** Ensure both Google Play Developer accounts are fully registered.
3. **Old owner:** Submit a transfer request via Google Play Console or Google Play Developer Support. Google may request additional documents such as DUNS numbers, contracts, or proof of sale.
4. **New owner:** Review and approve the transfer request.
5. **Google:** Google support team processes the transfer, typically within a few business days, but may take longer depending on account verification, subscription complexity, and payment setup.
6. **Old owner:** After Google completes the transfer, email [support@adapty.io](mailto:support@adapty.io) to transfer the app in Adapty. Provide the app name and new owner's email address.
7. **New owner:** After receiving the app in Adapty, complete the [Google Play integration guide](initial-android) to generate and configure all credentials under your account.
The transfer includes users, subscriptions, statistics, ratings, and store listing. Billing continuity is maintained for existing subscribers, but payouts switch to the new owner's merchant account only after the transfer completes. Payout reports and orders before transfer remain in the original account. Follow Google's [transfer guide](https://support.google.com/googleplay/android-developer/answer/6230247) for detailed requirements.
## Risk mitigation and timing
**What continues working during transfer:**
- Purchases (app-specific shared secret validates receipts)
- Renewals (shared secret remains valid)
- Existing subscriber access
- SDK continues functioning
**What temporarily stops working:**
- App Store Connect API calls (until new keys configured)
- Server notifications (until endpoint reconfigured)
- Analytics may have gaps during credential transition
**Recommended timing:**
- Complete transfers during low-traffic periods (3am-6am in your primary user timezone)
- Have new owner ready to configure credentials immediately after accepting store transfer
- Budget 15-30 minutes between accepting transfer and completing Adapty integration
**After completing transfer:**
- Test receipt validation immediately
- Monitor auto-renewal success rates for 48 hours
- Verify server notifications are reaching your systems
- Check that new purchases are being tracked correctly
## Verify transfer completed successfully
After completing both the Adapty and store transfers:
1. **Check Dashboard access**: New owner should see the app in their Adapty Dashboard.
2. **Verify API key connection**: Check that the new App Store Connect API Key or Google Play service account connects successfully in Adapty.
3. **Test SDK connection**: Run your app and verify Adapty SDK initializes without errors.
---
# File: installation-of-adapty-sdks
---
---
title: "Installation of Adapty SDK"
description: "Install Adapty SDKs for iOS, Android, and cross-platform apps."
---
You have three paths to get started depending on your preferences:
- **Follow platform-specific quickstart guides**: Guides contain production-ready code snippets, so implementation doesn't take long.
- [iOS](ios-sdk-overview.md)
- [Android](android-sdk-overview.md)
- [React Native](react-native-sdk-overview.md)
- [Flutter](flutter-sdk-overview.md)
- [Unity](unity-sdk-overview.md)
- [Kotlin Multiplatform](kmp-sdk-overview.md)
- [Capacitor](capacitor-sdk-overview.md)
- **Use LLMs**: Our docs are LLM-friendly. Read our [guide](adapty-cursor.md) on how to get the most out of using LLMs with the Adapty documentation.
- **Explore sample apps**:
- [iOS (Swift)](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples)
- [Android (Kotlin)](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app)
- [React Native (Basic example on pure RN)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/BasicExample)
- [React Native (Advanced example – useful for development, as it allows you to work with more complicated cases)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyDevtools)
- [React Native (Expo dev build)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo)
- [React Native (Expo Go & Web)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock)
- [Flutter (Dart)](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example)
- [Unity (C#)](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets)
- [Kotlin Multiplatform](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example)
- [Capacitor](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples)
---
# File: sample-apps
---
---
title: "Sample apps"
description: ""
---
To help you get started with Adapty SDK, we've prepared sample apps that demonstrate how to integrate and use its key features. These apps provide ready-to-use implementations of paywalls, purchases, and analytics tracking.
### Why use sample apps?
- **Quick Integration:** See how Adapty SDK works in a real app.
- **Best Practices:** Follow recommended implementation patterns.
- **Debugging & Testing:** Use the sample apps to troubleshoot and experiment before integrating Adapty into your own project.
### Available sample apps
- [iOS (Swift)](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples)
- [Android (Kotlin)](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app)
- [React Native (Basic example on pure RN)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/BasicExample)
- [React Native (Advanced example – useful for development, as it allows you to work with more complicated cases)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyDevtools)
- [React Native (Expo dev build)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo)
- [React Native (Expo Go & Web)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock)
- [Flutter (Dart)](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example)
- [Unity (C#)](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets)
- [Kotlin Multiplatform](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example)
- [Capacitor (React)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-react-example)
- [Capacitor (Vue.js)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-vue-example)
- [Capacitor (Angular)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-angular-example)
- [Capacitor (Advanced development tools)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools)
---
# File: create-product
---
---
title: "Create product"
description: "Step-by-step guide on creating new subscription products in Adapty for better revenue management."
---
The way you create products in Adapty depends on whether you already have them in stores:
- **[If products don't exist in App Store and/or Google Play yet, create them in Adapty and push to the stores right away](#create-product-and-push-to-store)**.
- **[If products already exist in App Store and/or Google Play, create them in Adapty and connect existing store products.](#create-product-and-connect-existing-store-products)**
## Create product and push to store
:::warning
Before you start, ensure you've configured the integration with the stores you need:
- [App Store](initial_ios.md)
- [Google Play](initial-android.md)
If you configured the App Store integration some time ago, ensure you've [added the App Store Connect API key](app-store-connection-configuration#step-6-add-app-store-connect-api-key) as well.
:::
2. Click **Create product** in the top-right corner. Adapty supports all types of products: subscriptions, non-consumable \(including lifetime\), and consumable.
3. Select **Create a new product and push to stores**.
4. Enter the following data:
- **Product name**: enter the name of the product to be used in the Adapty dashboard. The name is primarily for your reference, so feel free to choose a name that is most convenient for you to use across the Adapty Dashboard.
- **Access Level**: Select the [access level](access-level) to which the product belongs. The access level is used to determine the features unlocked after purchasing the product. Note that this list contains only previously created access levels. The `premium` access level is created in Adapty by default, but you can also [add more access levels](access-level).
- **Subscription duration**: select the duration of the subscription from the list.
- **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: The subscription duration.
- **Lifetime**: Use a lifetime period for the products that unlock the premium features of the app forever.
- **Non-Subscriptions**: For the products that are not subscriptions and therefore have no duration, use non-subscriptions. These can be unlocked for additional features, consumable products, etc.
- **Consumables**: Consumable items can be purchased multiple times. They could be used up during the life of the application. Examples are in-game currency and extras. Please consider that consumable products don’t affect access levels.
- **Price (USD)**: The product price in USD. This price will be used as a base to automatically calculate and set prices across all countries. You will be able to [customize the price for different countries and regions](edit-product#set-country-specific-prices) later.
5. Click **Save & Continue**.
6. Configure the product information for App Store if you plan to publish there:
- **Product ID**: Create a permanent unique ID for the product.
- **Product group**: Select an existing product group you've created in App Store Connect or click **Create new Product Group** and set its name. After Adapty creates it, you can select it from the dropdown.
- **Screenshot**: Upload a screenshot of the in-app purchase that clearly shows the item or service being offered. This screenshot is used for the App Store review only and isn't displayed on the App Store. See the screenshot size and format requirements [here](https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/).
7. Click **Push data to App Store**.
:::warning
If it is your first product for this app, you must manually submit it for review in App Store Connect. This won't be required later. Once the review is finished, the product status in Adapty will update automatically.
:::
8. Configure the product information for Google Play if you plan to publish there:
- **Base Product ID**: Create a permanent unique ID for the product.
- **Subscription**: Select an existing subscription group you've created in Google Play Console or click **Create new Product Group** and set its name and ID. After Adapty creates it, you can select it from the dropdown.
:::note
Grace Period and Account Hold Period will be automatically set to the default values as per Play Store rules. You can change them later in Google Play Console.
:::
9. Click **Push data to Play Store**.
10. Configure the introductory offer – free trial – by selecting its **Free duration** from the dropdown. For this initial setup, you can add an introductory free trial. Once the main product is approved by the stores, you can [add more offers](offers.md) (e.g., promotional, win-back) by linking their existing IDs from your store console.
11. Finally, click **Save** to confirm the product creation.
## Create product and connect existing store products
:::warning
Before you start, ensure you've:
- Configured the integration with the stores you need:
- [App Store](initial_ios.md)
- [Google Play](initial-android.md)
- Created products in the stores you need:
- [App Store](app-store-products.md)
- [Google Play](android-products.md)
**If you don't have any products created**, consider following the [Push to stores](#create-product-and-push-to-store) guide, so you create them both in Adapty and stores at the same time.
:::
2. Click **Create product** in the top-right corner. Adapty supports all types of products: subscriptions, non-consumable \(including lifetime\), and consumable.
3. Select **Connect an existing store product**.
4. Enter the following data:
- **Product name**: enter the name of the product to be used in the Adapty dashboard. The name is primarily for your reference, so feel free to choose a name that is most convenient for you to use across the Adapty Dashboard.
- **Access Level ID**: Select the [access level](access-level) to which the product belongs. The access level is used to determine the features unlocked after purchasing the product. Note that this list contains only previously created access levels. The `premium` access level is created in Adapty by default, but you can also [add more access levels](access-level).
- **Subscription duration**: select the duration of the subscription from the list.
- **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: The subscription duration.
- **Lifetime**: Use a lifetime period for the products that unlock the premium features of the app forever.
- **Non-Subscriptions**: For the products that are not subscriptions and therefore have no duration, use non-subscriptions. These can be unlocked for additional features, consumable products, etc.
- **Consumables**: Consumable items can be purchased multiple times. They could be used up during the life of the application. Examples are in-game currency and extras. Please consider that consumable products don’t affect access levels.
- **Price (USD)**: The product price in USD. If your product is already in the store, this value won't affect its actual price in the store; you can select any value from the list. Later, you can [customize prices for different regions](edit-product#set-country-specific-prices) right in the Adapty dashboard.
5. Click **Continue**.
6. Configure the product information from each store:
- **App Store:**
- **App Store Product ID:** This unique identifier is used to access your product on devices. Select it from the list. If you don't see it in the list, check its configuration in App Store Connect and ensure it's correct and belongs to this app.
- **Play Store:**
- **Google Play Product ID:** This is the product identifier from the Play Store. Select it from the list. If you don't see it in the list, check its configuration in Google Play Console and ensure it's correct and belongs to this app..
- **Base Plan ID:** This ID is used to define the base plan for the product in the Play Store. When adding a subscription's Product ID on the Play Store you have to provide a Base Plan ID. A base plan defines the essential details of a subscription, encompassing the billing period, renewal type (auto-renewing or prepaid), and the associated price. Please note, that within Adapty, each combination of the same subscription and different base plans is treated as a separate product.
- **Legacy fallback product**: A fallback product is used exclusively for apps using older versions of the Adapty SDK (versions 2.5 and below). By marking a product as backward compatible in the Google Play Console, Adapty can identify whether it can be purchased by older SDK versions. For this field please specify the value in the following format `
## Set country-specific prices
You can set different prices for different regions right in the Adapty dashboard, and these country-specific prices will be applied to your products in App Store Connect and/or Google Play Console automatically.
To set country-specific prices:
1. [Open the product for editing](#edit-product).
2. Click **Download** to get your current prices exported from stores in the correct format or create a new CSV file.
3. Update prices in the CSV file. Stick to the [format](#csv-file-format). If you leave a price for any country unchanged or don't include it in the file at all, nothing will happen. When you upload the CSV, Adapty compares prices and updates only those that differ.
4. In the **Edit** window, click **Upload** and select the CSV file.
5. If you want the changes to be active for existing subscribers as well, select **Apply to existing subscribers**.
6. Review the changes that will be applied and click **Save changes**.
### CSV file format
:::tip
You can reuse the same CSV file if you have similar products in one app or if you want to set the same prices across different apps.
:::
The easiest way to edit prices in CSVs is to [download a file with current prices and edit it directly](#set-country-specific-prices).
However, if you are doing it yourself, your file must contain the following columns:
- `region_name`
- `region_code`
- `app_store_currency`
- `app_store_requested_price`
- `play_store_currency`
- `play_store_requested_price`
Example:
```
region_name,region_code,app_store_currency,app_store_requested_price,play_store_currency,play_store_requested_price
United States,US,,8.99,,8.99
United Arab Emirates,AE,USD,8.99,AED,39.99
Germany,DE,USD,8.99,USD,8.99
```
## View audit log
Adapty logs all pricing changes for each product, so you can track who made changes and when. To view the audit log:
1. Go to **[Products](https://app.adapty.io/products)** from the Adapty main menu.
2. Click three dots next to the product and select **Audit log**.
The audit log table shows each pricing change with the date, the team member's name and role, and the number of changes.
To download a detailed CSV breakdown of an event, click the download icon in that row.
---
# File: delete-product
---
---
title: "Delete product"
description: "Find out how to delete a subscription product in Adapty without disrupting your app's revenue flow."
---
You can only delete products that are not used in paywalls.
To delete the product:
1. Go to **[Products](https://app.adapty.io/products)** from the Adapty main menu.
2. Click the **3-dot** button next to the product and select **Delete**.
2. Enter the product name you're about to delete.
3. Click **Delete forever**.
---
# File: add-product-to-paywall
---
---
title: "Add product to paywall"
description: "Learn how to add and manage products on paywalls in Adapty."
---
To make a product visible and selectable within a [paywall](paywalls) for your app's users, follow these steps:
1. While [configuring a paywall](create-paywall), click **Add product** under the **Products** title.
2. From the opened drop-down list, select the products that will be shown to your customers. The list contains only previously created products. The order of the products is preserved on the SDK side, so it's important to consider the desired order when configuring the paywall. Additionally, you can specify an offer for a product if desired.
3. Click **Create as draft** or **Save and publish** depending on the status of the paywall.
Please keep in mind that after creation, it is not recommended to edit, add, or delete products to the paywall as this may affect the paywall metrics.
---
# File: app-store-offers
---
---
title: "Offers in App Store"
description: "Set up and manage App Store offers to increase user retention."
---
:::info
If you haven't configured the products in the App Store yet, check our [documentation](app-store-products) on how to do it.
:::
Offers in the App Store are special deals, trials, or discounts provided for in-app purchases. Developers use offers to provide users with exciting promotions, like discounted prices, free trials, or bundled offers. These promotions help attract and keep users engaged, increasing their conversion to paying users.
There are four offer types in the App Store, and Adapty supports them all:
- **Introductory offers (Free or discounted subscription periods for new users)**:
- Free or discounted subscription periods
- Only new users are eligible (those who haven't activated an introductory offer or subscribed before)
- You don't need to link them to products in Adapty. They will be automatically applied for eligible users who purchase corresponding products.
- **Promotional offers**:
- Free subscription periods, percentage discounts, fixed price discounts
- Any user can be eligible
- Adapty automatically applies promotional offers for eligible users, but you will need to add them to your products and paywalls in Adapty.
- **Win-back offers**:
- Free subscription periods or percentage discounts
- Only churned users are eligible
- Adapty automatically applies win-back offers for eligible users, but you will need to add them to your products and paywalls in Adapty.
- **Offer codes**: For more information and a workaround, see the [guide](https://adapty.io/docs/making-purchases#redeem-offer-code-in-ios).
:::important
To use these offers, you have to [upload your subscription key](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) to Adapty dashboard, so Adapty can sign the offers.
:::
## Introductory offers
Adapty automatically applies introductory offers on iOS if the user is eligible.
So, to enable introductory offers for products you are selling, you only need to create them in App Store Connect:
1. Open your app in App Store Connect and switch to **Monetization > Subscriptions** from the left menu.
2. Select a subscription group and navigate to the subscription you need. Note that it must have duration set up.
3. Click **View all Subscription Pricing** and switch to the **Introductory offers** tab. Click **Set up introductory offer**.
4. Select countries and regions where the introductory offer will be available.
5. Select start and end dates for the introductory offer. If the introductory offer has no specific end date, you can just select **No end date**. Click **Next**.
6. Select the introductory offer type. Depending on what you select, you also need to define the offer duration and price. Read more in the [Apple documentation](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-introductory-offers-for-auto-renewable-subscriptions).
7. Review your choice and click **Confirm**.
As you finish this setup, you don't need to do anything additional in Adapty. The offer will activate for eligible users purchasing the product. So, ensure you are showing a paywall with this product only to those users who are supposed to be eligible for the offer.
## Promotional offers
In Adapty, promotional offers are applied automatically if users are eligible. You need to set them up in App Store Connect and add to your product and paywall in Adapty:
1. Open your app in App Store Connect and switch to **Monetization > Subscriptions** from the left menu.
2. Select a subscription group and navigate to the subscription you need. Note that it must have duration set up.
3. Click **View all Subscription Pricing** and switch to the **Promotional offers** tab. Click **Set up promotional offer**.
4. Set the promotional offer details. Note that these values can't be used only once.
- **Promotional offer reference name**: The promotional offer name. It won't be visible to your users.
- **Promotional offer identifier**: The promotional offer identification code. You will use it to add the offer to Adapty.
5. Select the promotional offer type that will define how your users will be charged for it depending on whether it is free or paid. Depending on what you select, you also need to define the offer duration and price. So, if you want to make a discount, select **Pay as you go** or **Pay up front**. For a free subscription period, select **Free**. Read more in the [Apple documentation](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-promotional-offers-for-auto-renewable-subscriptions).
6. If needed, set different prices for different countries and regions and click **Next**.
7. Review your choice and click **Confirm**.
8. Proceed with [adding the promotional offer to Adapty](create-offer).
## Win-back offers
:::important
Before you can create a win-back offer, your subscription must be approved by App Review.
:::
In Adapty, win-back offers are applied automatically if users are eligible. You need to set them up in App Store Connect and add to your product and paywall in Adapty:
1. Open your app in App Store Connect and switch to **Monetization > Subscriptions** from the left menu.
2. Select a subscription group and navigate to the subscription you need. Note that it must have duration set up.
3. Click **View all Subscription Pricing** and switch to the **Win-back offers** tab. Click **Create offer**.
4. Set the win-back offer details. Note that these values can't be used only once.
- **Reference name**: The offer name. It won't be visible to your users.
- **Offer identifier**: The offer identification code. You will use it to add the offer to Adapty.
5. Set the win-back offer details. Read more in the [Apple documentation](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-win-back-offers/).
6. Review your choice and click **Confirm**.
7. Proceed with [adding the offer to Adapty](create-offer).
## Next steps
After you've added offers, proceed with the setup:
- If you have **apps in Google Play as well**, go to the [Google Play guide](google-play-offers.md).
- If you have **promotional and/or win-back offers in the App Store**, follow [this guide](create-offer) to add them to Adapty.
- If you have **only introductory offers in the App Store** and don't have any promotional or win-back offers, you are all set, but these sections may be useful to you:
- [Working with offers in the Adapty Paywall Builder](create-offer#paywall-builder)
- [How Adapty works with offers](create-offer#how-adapty-works-with-offers)
---
# File: google-play-offers
---
---
title: "Offers in Google Play"
description: "Configure Google Play offers to improve app monetization and retention."
---
In Google Play, offers of any type (free trials or discounted payments) are added as **offers**. To create an offer, you must first, create a subscription and add an auto-recurring base plan.
Offers are always created for base plans in subscriptions. In the screenshot below, you can see a subscription `premium_access`(1) with two base plans: `1-month` (2) and `1-year` (3).
To create an offer in Google Play Console:
1. Click **Add offer** and choose the base plan from the list.
2. Enter the offer ID. It will be later used in the analytics and Adapty dashboard, so give it a meaningful name.
3. Choose the eligibility criteria:
1. **New customer acquisition**: the offer will be available only to new subscribers if they haven't used this offer in the past. This is the most common option and should be used by default.
2. **Upgrade**: this offer will be available for the customers upgrading from the other subscription. Use it when you want to promote more expensive plans to your existing subscribers, for example, customers upgrading from the bronze to the gold tier of your subscription.
3. **Developer determined**: you can control who can use this offer from the app code. Be cautious using it in production to avoid possible fraud: customers can activate a free or discounted subscription over and over again. A good use case for this offer type is winning back churned subscribers.
4. Add up to two pricing phases to your offer. There are three available phase types:
1. **Free trial**: the subscription can be used for free for a configured amount of time (minimum 3 days). This is the most common offer.
2. **Single payment**: the subscription is cheaper if the customers pay upfront. For example, normally a monthly plan costs $9.99, but with this offer type, the first three months cost $19.99, a 30% discount.
3. **Discounted recurring payment**: the subscription is cheaper for the first `n` periods. For example, normally a monthly plan costs $9.99, but with this offer type, each of the first three months costs $4.99, a 50% discount.
An offer can have two phases. In this case, the first phase must be a Free trial, and the second one is either a Single payment or a Discounted recurring payment. They would be applied in this order.
:::important
Please note that paywalls created with the Adapty Paywall Builder will display only the first phase of a multi-phase Google subscription offer. However, rest assured that when a user purchases the product, all offer phases will be applied as configured in Google Play.
:::
5. Activate the offer to use it in the app.
6. Proceed with [adding the offer to Adapty](create-offer).
:::note
Offer IDs can be the same for different base plans.
:::
## Next steps
After you've added offers, proceed with the setup:
- If you have **apps in App Store as well**, go to the [App Store guide](app-store-offers.md).
- If you have **apps only in Google Play**, follow [this guide](create-offer) to add offers to Adapty.
---
# File: create-offer
---
---
title: "Add offers to Adapty"
description: "Create and manage special subscription offers using Adapty’s tools."
---
Adapty allows you to offer trials or discounts to new, existing, or churned subscribers.
After you have set them up in App Store Connect or Google Play Console, you need to add them to Adapty in two steps:
1. [Add offers to products in Adapty using the offer IDs from stores.](#1-create-offer)
2. [Add offers to paywalls so they can be applied.](#2-add-offer-to-paywall)
:::warning
Introductory offers from the App Store are applied automatically if the user is eligible. Do not add them to products in Adapty. You only need to follow this guide if you are working with promotional or win-back offers from App Store and any offers from Play Store.
:::
## 0. Before you start
Before you start setting up offers in Adapty, ensure the following:
1. You have created all the offers you need in the store:
- [App Store](app-store-offers.md)
- [Google Play](google-play-offers.md)
2. You have created the [products](create-product.md) in Adapty and added their IDs.
3. For App Store: You have uploaded [the in-app purchase key for promotional offers](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers).
## 1. Add offer to product in Adapty
Once your promotional offer (for both the Play Store and App Store) or Win-back offer (for the App Store) is set up in the app stores, add it to Adapty is simple:
1. Open [**Products**](https://app.adapty.io/products) from the main menu in Adapty. Find the product to which you want to add an offer.
2. Find the product you want to add an offer to. In the **Actions** column, click the **3-dot** button next to the product and select **Edit**.
3. In the **Edit product** window, click **+** and select **Add offers**.
4. Click **Add offer**.
5. Then enter the offer details for the product.
Here are the fields for the offer:
- **Offer name**: Give the offer a name to help identify it in Adapty. Use any name that’s convenient for you.
- **App Store Offer type**: Select the type of App Store offer you’re adding: Promotional or Win-back. (Introductory offers don’t need to be added—they apply automatically if available.)
- **App Store Offer ID**: This is the unique ID for the offer [that you set in the App Store](app-store-products).
- **Play Store Offer ID**: Similarly, this is the unique ID for the offer [that you set in the Play Store](android-products).
:::tip
If the **App Store Offer ID** or **Play Store Offer ID** field is not active, switch to the **Products** tab and select a product ID.
:::
6. (optional) Add more offers if needed by clicking **Add offer**.
7. Click **Save** to add the offers to the product.
## 2. Add offer to paywall
:::info
You can't add offers to paywalls in the **live** status. If you want to add an offer to an existing paywall, [duplicate](duplicate-paywalls.md) it and configure products in a new paywall.
:::
To make an offer visible and selectable within a [paywall](paywalls) for your app's users, follow these steps:
1. When creating or editing a paywall, in the **General** tab, add a product to which you've just added the offer.
2. Choose an offer you [created earlier](create-offer) for this product from the **Offer** list. The list is available only for the products that have offers.
3. If needed, add more products and offers, but you can add only one offer for each product.
### Paywall builder
:::info
Paywalls created with the Adapty Paywall Builder will display only the first phase of a [multi-phase Google subscription offer](https://support.google.com/googleplay/android-developer/answer/12154973). However, rest assured that when a user purchases the product, all offer phases will be applied as configured in Google Play.
:::
When you create a paywall in the Adapty Paywall Builder, you have some more customization options for trials:
- **Toggle**: In **Products**, set **Product grouping** to **Toggle** and add product-offer combinations for each toggle state. The most common use case is to include a product without an offer (e.g., direct purchase without a free trial) and a different product with an offer attached.
- **Dynamic text**: You can make the purchase button text change depending on the offer available to the user seeing the paywall. You can set different texts for **Default**, **Free trial**, **Pay as you go**, and **Pay up front**.
## How Adapty works with offers
Note the following about how offers work in Adapty:
- When a user is eligible for an offer, Adapty automatically applies the offer you've configured when the user makes a purchase.
- If a product has both an introductory offer and promotional offers configured in the App Store, eligible users will receive the introductory offer first. After its period ends, if the user is still eligible for the promotional offer and you've configured this offer in Adapty, it will be applied when they attempt to purchase the product again.
- If you want more control over how offers are applied or need to sell your product without offers in certain cases, you have several options:
- Configure eligibility criteria in the App Store or Google Play Console
- Create a separate product without offers in the App Store or Google Play Console
- Create a separate product without offers in Adapty, add paywalls containing both product variants to a [placement](placements.md), and use audience [segments](segments.md) to control which paywall is displayed to different users. For example, you can create segments based on the **Subscription product** or **Paid access level**, or use [custom attributes](profiles-crm.md) to implement your own logic.
---
# File: create-paywall
---
---
title: "Create paywall"
description: "Learn how to create high-converting paywalls using Adapty’s Paywall Builder."
---
A [paywall](paywalls) is an Adapty configuration that defines which products to offer. In Adapty, paywalls are the only way to retrieve products in your app.
You need a paywall regardless of how you display it:
- [**Paywall Builder**](adapty-paywall-builder): Design a screen in the no-code editor. Adapty renders it and handles purchases.
- **Custom paywall**: Implement your own UI and use the paywall configuration to retrieve the products.
Once created, assign the paywall to a [placement](placements) — placements control which paywall users see. A live paywall's products are fixed, so its metrics always reflect the same combination, letting you compare performance across different products and pricing sets.
## Next steps
After you have created your first paywall:
1. Add it to a [placement](placements.md). Placement IDs will be the only hardcoded entities. You will be using them to get products to sell.
2. The way you work with the paywall next depends on your implementation:
- If you want to use the [Adapty Paywall Builder](adapty-paywall-builder.md), design the paywall in the no-code editor. Adapty will render the paywall and handle the purchase logic, while you will only need to display the paywall in the app code.
- If you have a custom paywall you want to use, see our guides for implementing in-app purchases with Adapty for your platform:
- [iOS](ios-implement-paywalls-manually.md)
- [Android](android-implement-paywalls-manually.md)
- [React Native](react-native-implement-paywalls-manually.md)
- [Flutter](flutter-implement-paywalls-manually.md)
- [Unity](unity-implement-paywalls-manually.md)
- [Kotlin Multiplatform](kmp-implement-paywalls-manually.md)
---
# File: paywall-builder-templates
---
---
title: "Paywall template"
description: "Use Adapty’s Paywall Builder templates to create high-converting paywalls."
---
Ready-made paywall templates are professionally designed and tailored to streamline your paywall creation process. These templates are crafted by expert designers to help you present your products attractively with minimal effort.
Simply add your logo, infuse your brand personality, and you're all set to captivate your audience and drive sales!
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder templates](paywall-builder-templates-legacy).
:::
## Why use paywall templates
Templates serve as a fantastic starting point, offering beautifully structured layouts and visual appeal. You can either use them as they are or make slight modifications to align them with your brand's aesthetics.
Here's why ready-made templates are a smart choice:
- **Time-Saving**: Quickly set up a professional-looking paywall without the need for extensive design work.
- **Consistency**: Ensure a cohesive look that aligns with proven design standards.
- **Customizability**: Personalize each template with your brand elements to make it uniquely yours.
For those who prefer a hands-on approach, templates with a minimal design offer a blank canvas. These templates come with basic placements, making it easier for you to unleash your creativity and build a paywall from scratch using Adapty's versatile, feature-rich, and user-friendly Paywall Builder.
## Choose paywall template
When creating a new paywall, Adapty offers a selection of templates. You can easily switch between templates at any moment after that.
However, it's important to note that replacing a template will discard any changes made to your paywall design. To avoid losing your work, we recommend duplicating the paywall before changing templates so you can return to the saved paywall if needed.
1. Go to the **Layout settings** of the paywall.
2. Click **Change template**.
3. In the opened **Choose template** window, browse and select a new template.
4. Click **Choose** to confirm the template change. Please note that replacing a template will discard any changes made to your paywall design.
---
# File: paywall-generator
---
---
title: "Paywall generator"
description: "Generate paywalls with AI and launch them quickly."
---
:::info
The Paywall AI generator is only available for apps published on the Apple App Store.
:::
You can create a unique, high-converting paywall tailored to your app in just seconds using our built-in AI generator.
This way, you can launch your paywalls quicker and stop wasting time thinking where to start.
:::note
You can't prompt which elements will be inside the paywall. Mainly, the paywall generator is your way to get perfect visuals by creating text and images.
:::
## Generate paywalls
To generate a paywall:
1. In the **Layout settings** of the paywall, click **Change template > Generate template**. Or, for a new paywall, click **Generate paywall** from the **Builder & Generator tab**.
2. Write your prompt in the input field. Or, select one of the predefined prompts to test what the paywall generator can create for your app. See [the tips below](#how-to-write-prompts-for-paywall-generator) to learn how to write the most effective prompt to get a nice-looking production-ready paywall.
3. The paywall generator will pull information about your app from the App Store a generate a relevant paywall with the products you've added. If you encounter any issues, ensure you have [**Apple App ID** in the **App settings**](app-store-connection-configuration#step-1-provide-bundle-id-and-apple-app-id).
4. Choose one of the five generated templates and click **Pick & Open in Builder**, or chat with the generator like you do with your favorite AI agents to improve the generation result.
:::important
You can generate up to 10 sets of templates per day for an app. Each chat can contain up to 10 generations. If you reach this limit, create a new chat and use context from the previous chat to write a detailed prompt.
:::
5. Chats are organized as threads and saved in your app account. You can get back to chats anytime later and try other templates from threads. You can always check which prompts worked the best or adjust some templates you used before.
## How to write prompts for paywall generator
These tips will help you write prompts for generating a production-ready paywall:
- The more details you specify in the prompt, the better.
- **Bad**: Make my paywall look modern
- **Good**: Create a modern, minimalistic paywall with a light background, rounded buttons, and subtle gradients.
- Don't describe your app in detail – Adapty pulls this automatically from the App Store:
- **Bad**: Create a Christmas paywall for my app that teaches people how to draw.
- **Good**: Create a festive Christmas-themed paywall with warm colors, hand-drawn snowflakes, and a cheerful “Unlock all lessons” headline.
- Describe visuals and text, not layout or structure:
- **Bad**: Add user reviews to the carousel.
- **Good**: Include a short quote from a happy user, like “This app completely improved my drawing skills!”, near the bottom of the screen.
- Specify the image or illustration style clearly.
- **Bad**: Add an image of people exercising.
- **Good**: Add a flat-style illustration of two people doing yoga on mats in a bright, minimal studio. Use soft pastel colors to match a wellness theme.
---
# File: manage-paywall-ui-elements
---
---
title: "Manage paywall UI elements"
description: "Customize and manage paywall UI elements to enhance user experience."
---
After choosing a template, the elements of it will be displayed in the left pane. Use this pane to organize the elements on your paywall. The elements will appear on the paywall in the same order as they do in the left pane.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder](adapty-paywall-builder-legacy).
:::
## Add element
To add an element to your paywall above the layout:
1. Click the **Add Element** button in the left pane.
2. Choose the element you want to add. The new element will appear in the list above the **Footer**.
To add an element to a compound element:
1. Click the **Plus** button next to the compound element.
2. Select the element you want to add.
## Rename paywall element
To rename an element:
1. Click the element in the left pane to open its details.
2. Click the ellipse button in the right pane and choose the **Edit** option.
3. Type the new name of the element and press **Enter**.
## Duplicate element
To duplicate an element :
1. Click the element in the left pane to open its details.
2. Click the ellipse button in the right pane and choose the **Duplicate** option.
The duplicated element, with "Copy" added to its name, will appear in the left pane as a complete duplicate of the original.
## Move element
To move an element: Drag and drop the element to its new position on the layout or within a compound element.
A purple line indicates an available position for the element, while a red line shows an inaccessible position.
## Hide / show element
Even though you have already created and configured an element, you can temporarily hide it from the paywall. That is convenient if you plan to add it later without losing all the configuration you made. After hiding an element, the paywall looks as though the element was never added, all alignments and spaces are recalculated and redrawn.
To hide an element:
1. Click the element in the left pane to open its details.
2. Click the ellipse button in the right pane and choose the **Hide** option.
The hidden element is marked in both the main pane - as a note and in the left pane if you choose it.
To show the element again, click the ellipse button in the right pane and choose the **Show** option.
## Delete element
To delete an element from the paywall:
1. Click the element in the left pane to open its details.
2. Click the ellipse button in the right pane and choose the **Delete** option.
---
# File: paywall-device-compatibility-preview
---
---
title: "Preview paywalls"
description: "Preview paywall compatibility across devices for an optimized experience."
---
:::important
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder device compatibility preview](paywall-layout-and-products-legacy#device-compatibility-preview).
:::
You have two ways to preview your paywall on different screen types:
- **Preview on devices**: Ensure everything looks as intended on real devices at any stage of development.
- **Preview in the Adapty dashboard**: Preview your paywall while designing it.
## Preview on devices
To preview your paywall on a real device:
1. [Download the Adapty app from the App Store](https://apps.apple.com/us/app/adapty/id6739359219).
2. In the paywall builder, click **Test on device**.
## Preview in the Adapty dashboard
You can preview your paywall on different screen types using the panel on the right side of the paywall builder. This helps ensure your paywall looks great across various devices and screen sizes.
From here, you can:
- Select the device to preview your paywall on.
- Switch between horizontal and vertical preview modes — especially useful for paywalls designed for iPad.
- Zoom in or out of the preview.
- Preview [tags variables for product info](https://adapty.io/docs/paywall-builder-tag-variables#how-to-use-tag-variables).
:::tip
Set the [maximum width](https://adapty.io/docs/paywall-layout-and-products#content-layout) of elements to optimize layout on iPads.
:::
---
# File: paywall-layout-and-products
---
---
title: "Paywall layout"
description: "Design paywall layouts and manage products in Adapty for better conversion."
---
After selecting a template for your paywall in Adapty's Paywall Builder, you can customize the paywall's visual appearance to match your brand's style. The Layout settings provide a variety of controls for adjusting the layout, background, and overall look of the paywall. Let's explore these settings: The layout settings control the basic aspects of the paywall, including the template, background color, default fonts, purchase flow, content layout, and top buttons.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder paywall layout](paywall-layout-and-products-legacy#layout).
:::
## Purchase flow
Decide how users will complete purchases. There are two options:
- **Products as list + purchase button**: Users select products first, then click the buy button to start the purchase.
- **Products as purchase buttons**: Each product is a button, and the purchase begins when the user clicks a product button.
## Background color
Maintain visual consistency by setting a default background for your paywall. Use the **Background color** field in the **Layout settings**. Click the colored square to open the configuration window, where you can choose a solid color or a gradient in separate tabs.
## Font settings of your paywall
It's important to keep your paywall visually consistent with the rest of your app — and one of the biggest visual factors is the font that you're using. You can choose to simply have a system font for your paywall (SF Pro for iOS, Roboto for Android), use one of the available common fonts, or upload your own custom font:
Font settings in the **Layout settings** apply to all paywall components by default. You can override these settings for specific elements, such as text boxes or lists, when editing those elements individually. If you change the default font in the **Layout settings**, it will not affect elements with individual fonts. Learn how to upload a custom font [here](using-custom-fonts-in-paywall-builder).
## Content layout
You don't have to manually fine-tune margins and width for each content element of the paywall. Go to the **Content layout** to adjust all the following settings for all content elements at once:
- **Default child margin**: Defines space around each child element.
- **Spacing**: Defines space between elements inside a layout.
- **Max width**: Sets the maximum width of elements to optimize layout on iPads. We recommend 600pt for a clean, balanced layout.
:::warning
Max width parameter is only available starting with Adapty SDK v3.7.0 and higher.
:::
To adjust the layout for a specific element—such as setting the maximum width for the footer—go to the **Layout** section under **App Icon, Header, Feature List, Products**, or **Footer**.
## Top buttons
Add up to 2 top buttons to your paywall to provide users with options like closing the paywall. Customize their appearance and behavior as follows:
1. Enable the **Top Button** or **Top Button 2** toggle.
2. Choose the button's look and position. The preview will update instantly.
| Button setting | Description |
|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Action | Choose the action that the paywall should perform when a user clicks this button. If you choose standard actions, the paywall will generate a standard event you will be able to handle in a standard way in your mobile app code.
If you choose a custom action, you will need to process the action by its `CustomActionID` in your mobile app code.
| | Style | Choose if you want the button to look like an icon or have a text. If you choose an icon, choose the icon type in the | 3. To delay the appearance of the button, adjust the **Show after delay** slider.
---
# File: paywall-head-picture
---
---
title: "Paywall hero image"
description: "Customize your paywall with a head picture to improve conversion rates in Adapty."
---
The hero image is the star of your paywall, setting the tone, establishing the theme, and capturing users' attention right from the start. This image plays a crucial role in shaping the look and feel of your paywall on both iOS and Android platforms.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder head picture](paywall-layout-and-products-legacy#main-image-and-sizing).
:::
## Hero image format and size
Your main image is the centerpiece of your paywall's design, essential for captivating users and driving them to take action. Follow these guidelines to ensure your hero image is effective and visually appealing:
- **Formats**: JPG and PNG.
- **Recommended Size**: Files up to 2 MB for faster loading.
- **Image Composition**: Photos with the main object centered and surrounded by ample space usually communicate your message effectively.
- **Impactful Visuals**: Emotional or vibrant photos work well.
- **Graphics Use**: Ideal for adding visual appeal, with separate spaces reserved for text.
You have control over the sizing of the main image, adjusting its proportions to achieve the desired visual balance on your paywall. Specify the image size as a percentage of the total screen area for perfect alignment.
## Hero picture layout options
The **overlay hero image** adds a layer of depth and dynamism to your paywall. Positioned as a fixed background at the bottom, it creates a stunning effect as other elements scroll over it. This makes the hero image appear stationary, providing a visually engaging experience as users scroll through the content.
The **transparent layout** delivers a bold, full-screen hero image that instantly captures attention. This layout is perfect for showcasing a limited selection of products or content, filling the entire screen and making a powerful, direct impact without the need for scrolling.
:::note
Use the transparent layout for minimal content display, as it doesn’t involve scrolling, making your message clear and impactful.
:::
The **flat layout** mimics a seamless landing page, presenting all elements in a continuous, scrollable layer. Users enjoy a smooth, cohesive narrative as they scroll through the content, perfect for integrating your products or stories effectively in a unified flow.
:::note
Ideal for storytelling or presenting a series of offerings, the flat layout lets you create a compelling sequence that captivates users.
:::
## Hero image mask
The **mask type** defines the shape of the main image, allowing you to apply creative effects that enhance the visual presentation. For flat or overlay image layouts, choose from various mask types to suit your design.
Adjust the roundness of the image mask using numerical values to achieve the perfect look for your hero image.
## How to remove a hero image
To remove a hero image from a paywall:
1. Open the **Hero image** element.
2. Change its height to zero.
---
# File: paywall-video
---
---
title: "Paywall hero video"
description: "Enhance paywalls with video content to boost engagement in Adapty."
---
Adding a video clip to your paywall is a great way to engage users and increase conversions. Videos can explain your offers more clearly, highlight key features, and create a more dynamic, visually appealing experience that grabs attention.
:::warning
Hero video is supported on Adapty SDK:
- iOS: starting with v3.1.0
- Android: starting with v3.1.1
- React Native: starting with v3.1.0
- Flutter: starting with v3.2.0
- Unity: starting with v3.3.0
If the video isn't supported or in fallback cases, the first frame of the video will be shown instead.
:::
Add the **Hero video** in place of the **Hero image** element:
1. First, switch to the video mode.
2. Then, drag and drop your video file into the **Video file** area.
## Supported formats
| Specification | Details |
|----------------|---------------|
| Extensions | MP4 and WEBM |
| Minimum size | 640х480 |
| Maximum length | 30 sec |
| Audio | Not supported |
---
# File: paywall-product-block
---
---
title: "Paywall product list"
description: "Discover how to configure paywall product blocks in Adapty to optimize in-app purchases."
---
The product list is a key element of your paywall that showcases your offerings in an organized, attractive manner. This list is crucial for guiding users toward making a purchase.
The **Content** tab contains the products that will be displayed in the paywall. These are the same products you added to the paywall when you created it.
You can adjust the list of products. This will affect the product list in the **General** tab of the paywall.
After you review the list of products:
1. Choose which product should be preselected by default in the **Selected product** field.
2. Define how a product should look if it is selected or not in the **Style** tab of the **Products** section.
3. Configure the overall view of the block in the **Layout** tab or [add products groups](#add-products-group) to combine layouts.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder paywall products](paywall-layout-and-products-legacy#products).
:::
## Product view customisation
Enhancing the visual appeal of specific products can significantly rebalance user attention. Highlighting a product or special offer can encourage users to focus on it. Let’s look at some powerful customization options.
### Product badge
A product badge is a small label that can be added to a product. This badge can provide additional promotional information and direct user choice. The badge size automatically adjusts to fit the text, and its position is optimized for your paywall layout.
To add a product badge:
1. Turn on the **Product badge** toggle in the setting of a specific product.
2. Customize the badge view and text to suit your design needs.
### Selected product
For the **Products as list + purchase button** purchase flow, you can **preselect** a product to gently nudge users towards it. This can be especially effective in directing user choice.
If you add several [product groups](#add-products-group), the same **Selected product** option will be applied to all groups.
To preselect a product:
1. Open the **Products** element.
2. In the **Content** tab, choose the product you want to preselect from the **Selected product** drop-down list.
3. Adjust the view of the selected product as well as the default view of other products in the **Style** tab if necessary.
### Highlighted product
For the **Products as purchase buttons** purchase flow, you can **highlight** a preferred product to make it the primary choice, drawing immediate user attention.
To highlight a product:
1. In the left pane, choose the product you want to highlight.
2. In the **Style** subsection, adjust the design to make the product stand out more.
### Product offers
Each product can feature different text for offers in the **Text** subsection. The **Default** tab contains the text displayed without an offer.
This is a good place to use:
- [tag variables](paywall-builder-tag-variables) for dynamic, localized content
- [custom tags](custom-tags-in-paywall-builder) for your own dynamic content
Start typing with a triangle bracket, and Adapty will suggest available tag variables to insert localized data from the stores.
## Switch between 2 product sets by trial toggle
To create a versatile user experience, you can allow users to switch between two sets of products using a toggle. This is especially useful for differentiating between standard products and trials.
To add a toggle:
1. In the **Products** element, change the **Products grouping** option to **Toggle (for free trials and other offers)**. This will add two subsections: **Toggle on** and **Toggle off**.
2. Add products to both subsections to create views for when the toggle is on or off.
3. In the **Toggle** element, set the **Default state** to choose whether the toggle should start as on or off in the paywall.
## Switch between product sets by tab
Paywall tabs let you group your products into categories, highlighting all possible options for your users. They're especially helpful if:
- Your app offers multiple weekly, monthly, or yearly plans
- You have different tiers like Plus, Gold, or Premium
You can also add elements like feature lists in tabs to help users see the differences between tiers.
To add tabs:
1. In the **Products** element, set **Products grouping** to **Tabs (for comparing plan groups)**. This will split your products into two initial tab groups.
2. If you need more tabs, click **Add tab group**.
3. Organize your products within these tabs.
4. Open the first tab group, and in the **Tab title**, enter the name that will appear on the paywall.
5. Give an internal tab name in a separate field for easy reference. This name won’t be visible on the paywall but can help you identify the tab in lists.
6. Repeat steps 4-5 for each tab.
7. Choose which tab will be active when the user opens the paywall. Go to **Tab control** and select the default tab from the **Selected tab** list.
## Show extra products under a button
To keep your paywall simple, you can hide some products or product groups under a button (like "View more plans" or any label you prefer).
This helps users focus on your top options first while still allowing them to explore other plans if they want.
It's a great way to make the paywall cleaner and improve conversions.
Here’s how:
1. In the **Products** element, set the **Products grouping** option to **Button (for more alternative plans)**. This will split your products into two groups: **Shown** and **More plans**.
2. Distribute your products between these groups. **Shown** is for products you want displayed immediately. **More plans** is for products hidden behind the button, shown only when users click it.
3. Customize the text and layout of the button in the **View more plans** element to suit your needs.
These options help you build a clear, visually appealing product list that guides users toward purchase.
## Show extra plans in a bottom sheet
To simplify your paywall, you can hide some products and display them only when users click a button (like "View More Plans" or any label you choose).
This action opens a sliding bottom sheet with the hidden products.
This setup helps users focus on your main options first while still giving them the flexibility to explore additional plans if they're interested.
It's an effective way to declutter the paywall and potentially boost conversions.
Here’s how:
1. In the **Products** element, set the **Products grouping** option to **Bottom Sheet (for more alternative plans)**. This will split your products into two groups: **Shown** and **More plans**.
2. Distribute your products between these groups. **Shown** is for products you want displayed immediately. **More plans** is for products that are initially hidden and shown only when users click the button.
3. Customize the text and layout of the button in the **View More Plans** element to fit your design and messaging.
4. The bottom sheet will automatically use the same product list display format as your main paywall, whether products are separate purchase buttons or each product acts as a button. You can customize the bottom sheet layout, text, style, and default product selection.
These options help you create a simple, user-friendly product list.
## Add products group
If you want to apply both vertical and horizontal layouts to different products or add text between products, you can add another products group.
:::note
Adding a products group disables the [Products grouping](#switch-between-2-product-sets-by-trial-toggle) option. Choose between adding another products group or grouping products inside the same block.
:::
To add a products group:
1. Click **Add element** or **+** on the **Footer**.
2. Select **Products**.
3. Add products. Since you can't have the same product in different groups, you need to first delete it from another group.
---
# File: paywall-buttons
---
---
title: "Paywall button"
description: "Customize paywall buttons in Adapty to enhance user interactions and increase conversions."
---
:::warning
**Only purchases and restorations are handled automatically.** All the other button actions, such as closing paywalls or opening links, require implementing proper responses in the app code:
- [iOS](handle-paywall-actions.md)
- [Android](android-handle-paywall-actions.md)
- [React Native](react-native-handle-paywall-actions.md)
- [Flutter](flutter-handle-paywall-actions.md)
- [Unity](unity-handle-paywall-actions.md)
:::
A paywall button is a UI element that lets users:
- Buy products
- Sign in
- Restore purchases
- Close the paywall
- Trigger custom actions (e.g., open another paywall)
:::info
This section describes the new Paywall Builder, which works with:
- iOS, Android, and React Native SDKs version 3.0 or higher
- Flutter and Unity SDKs version 3.3.0 or higher
:::
### Purchase buttons
Purchase buttons:
- Connect to selected products in your paywall
- Start the purchase when tapped
When you add a purchase button to your paywall, it automatically processes purchases your users make. So, you don't need to handle purchases in the app code.
:::note
You can attract more attention to purchase buttons by animating them. The Paywall builder currently supports **Arrow** and **Pulse** animation types. Note, that, to add the **Arrow** animation, first, you need to configure the **Arrow icon** in the **Content** section.
Each animation lets you choose an easing option (Linear, Ease In, Ease Out, Ease In Out) to control how it speeds up or slows down.
Animations are available in the Adapty iOS, Android, React Native, and Flutter SDKs starting from version 3.10.0. Follow the [migration guide](migration-to-android-310.md) for Android.
:::
### Links
To comply with some store requirements, you can add links to:
- Terms of service
- Privacy policy
- Purchase restoration
To add links:
1. Add a **Link** element in the paywall builder.
2. Add the `openUrl` handler to your code:
- [iOS](handle-paywall-actions.md)
- [Android](android-handle-paywall-actions.md)
- [React Native](react-native-handle-paywall-actions.md)
- [Flutter](flutter-handle-paywall-actions.md)
- [Unity](unity-handle-paywall-actions.md)
### Custom buttons
You need custom buttons to:
- Close the paywall (`close`)
- Open a URL (`openUrl`)
- Restore purchases (`restore`)
- Sign in (`login`)
- Trigger custom actions (e.g., open another paywall)
To make most buttons work, you need to **handle their action IDs in your code**:
- [iOS](handle-paywall-actions.md)
- [Android](android-handle-paywall-actions.md)
- [React Native](react-native-handle-paywall-actions.md)
- [Flutter](flutter-handle-paywall-actions.md)
- [Unity](unity-handle-paywall-actions.md)
For example, a close button needs the `close` action handler.
:::important
`close` is handled automatically in the iOS, Android, and React Native SDKs. `openUrl` is handled automatically in the iOS and Android SDKs. However, if needed, you can override the default behavior.
`restore` is always handled automatically.
:::
When handling custom actions in your code, you can implement scenarios like:
- Opening another paywall
- Running multiple actions in sequence (like close and open)
Note that you would need to build these scenarios using the action handling system - they're not built-in features.
---
# File: paywall-carousel
---
---
title: "Paywall carousel"
description: "Set up paywall carousels in Adapty to boost engagement and subscriptions."
---
A carousel is a dynamic set of swipeable cards that can be moved left or right, creating a captivating visual experience. It's a fantastic tool to craft paywalls that not only draw attention but also engage users with interactive content.
:::warning
Carousels are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall carousel functionality.
:::
Use the power of carousels to elevate your paywall's appeal:
1. **Add Cards**: Start by adding cards to your carousel, each one a canvas for your creative touch.
2. **Customize Cards**: Populate your cards with various elements—images, text, buttons, and more. The carousel's height setting ensures all cards are uniformly sized, delivering a sleek and consistent appearance.
Embrace the flexibility of carousels to showcase featured products, highlight exclusive offers, or narrate compelling stories. With this engaging element, your paywalls will not only stand out but also provide a seamless and immersive user experience.
---
# File: paywall-card
---
---
title: "Paywall card"
description: "Design and implement paywall cards in Adapty for better engagement."
---
A card is a paywall element that combines several other elements into a single block. The card itself may or may not be visible if this is not needed. To make it visible, add it a background or background picture, frame, etc.
:::warning
Paywall cards are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall card functionality.
:::
1. Add a card as a separate element to a paywall or to another paywall element, for example, to a carousel.
2. Add element you need in the card.
3. Configure the card's view: background, shape, frame, etc.
---
# File: paywall-timer
---
---
title: "Paywall timer"
description: "Use Adapty’s paywall timer feature to increase conversions and create urgency."
---
The paywall timer is a great tool for promoting special and seasonal offers with a time limit. However, it's important to note that this timer isn't connected to the offer's validity or the campaign's duration. It's simply a standalone countdown that starts from the value you set and decreases to zero. When the timer reaches zero, nothing happens—it just stays at zero.
:::warning
Paywall timers are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall timer functionality.
:::
You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec."
1. Add a timer as a separate element to a paywall or to another paywall element, like a card.
2. Configure the timer's settings: format and separator, start value, text before and after (if needed), color, font, spacing, etc.
## Timer mode
You can control how the timer behaves when users see it by using the **Timer mode** parameter. 3 standard modes work out of the box—just select the required option from the dropdown list:
| Mode | Description |
| ------------------------------------- | ------------------------------------------------------------ |
| **Reset timer on every paywall view** | The timer resets every time the user sees the paywall, starting from the initial value each time. |
| **Reset timer on every app launch** | The timer starts the first time the user sees the paywall and keeps counting in the foreground or background until the app is restarted. If the user sees the paywall multiple times in the same session, they’ll see the same timer counting down. Once the app is closed, the timer resets, and the next time the app is opened, the timer restarts from the beginning. |
| **Keep timer across app launches** | The timer starts the first time the user sees the paywall and keeps counting in the foreground or background, even if the app is closed. The user will see the same timer every time they return to the paywall, regardless of app or paywall restarts. |
| **Developer defined** | You can set up any timer you need directly in your mobile app code. Start by entering a **Timer ID**, then use it in your code as explained in the [How to set up developer-defined timers in your mobile app](paywall-timer#how-to-set-up-developer-defined-timers-in-your-mobile-app) section to configure the timer however you like. |
## What happens when the timer ends?
You can customize what happens when the timer runs out. Should it display another screen with a new opportunity? Or maybe show a different paywall? It requires some coding, but with our docs, you'll handle it.
1. Turn on the **Trigger custom action when the timer runs out** toggle.
2. Enter the ID of the action you want to trigger in the **Timer action ID** field.
3. Use this action ID in your app to define what should happen when the timer ends. Treat it like any other custom action, as explained in our **Handling Events: Actions** guide for [iOS](ios-handling-events#actions) and [Android](android-handling-events#actions).
## How to set up developer-defined timers in your mobile app?
To use custom timers in your mobile app, create an object that follows the `AdaptyTimerResolver` protocol. This object defines how each custom timer should be rendered. If you prefer, you can use a `[String: Date]` dictionary directly, as it already conforms to this protocol. Here is an example:
:::warning
Dark mode is supported:
- iOS: starting with v3.1.0
- Android: starting with v3.1.1
- React Native: starting with v3.1.0
- Flutter: starting with v3.2.0
- Unity: starting with v3.3.0
It’s also available in fallback paywalls.
:::
To set up dark mode for your paywall:
1. First, enable dark mode in the paywall’s **Layout settings**:
2. Now, you can configure light and dark modes separately. To switch to dark mode, turn on the **Dark Mode** toggle in the left paywall menu:
3. Once you’ve switched to dark mode, you can adjust the elements as needed. Dark mode lets you use a different image or video, as well as separate color and background options.
---
# File: paywall-builder-tag-variables
---
---
title: "Tag variables for product info in Paywall Builder"
description: "Use tag variables in Adapty’s Paywall Builder to personalize user experiences and boost sales."
---
Adapty’s Paywall Builder lets you customize all the text for your products and their offers. If you’re supporting multiple locales, we strongly recommend using variables.
### How it works
When you add tag variables from our list to your product texts, our SDK pulls in the pre-fetched localized data from the app stores to replace the tags. This ensures that the text on your paywall is always perfectly tailored for the correct locale.
**Example**: Let’s say you have a "Premium Subscription" available in both the US and Spain. In the US, it might display as "Premium Subscription for $4.99/month," while in Spain, it would show "Suscripción Premium por €4.99/mes."
Tag variables allow you to automatically localize these strings based on data directly from the store, ensuring that titles and prices are always accurate.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder tag variables](paywall-builder-tag-variables-legacy).
:::
### How to use tag variables
:::note
You can only use tag variables when describing products and offers in the Product component of the Paywall Builder.
:::
1. In the Paywal Builder’s left pane, select the product you want to customize.
2. Use variables from the [table below](paywall-builder-tag-variables#full-list-of-variables) in any text fields to describe the product and its offers.
4. Check the preview on the right side of the screen to ensure everything renders as expected.
:::note
The preview doesn’t use real values to replace your variables; those are only retrieved by our SDK on a device. However, it does display template data in the same format as the actual result. You can disable this behavior by clicking the eye icon in the bottom-right corner of the preview and turning off the **Tags preview values** toggle. The preview will then show the actual values of the variables:
:::
### Full list of variables
| Tag variable | Description | Example |
| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------- |
| `
2. After adding the custom tag, make sure to enter a fallback line. This fallback text will appear in your app if it doesn't recognize a particular custom tag, ensuring users won't see the tag displayed as code. The fallback text replaces the entire line containing the custom tag.
## How to use custom tags in your mobile app
To use custom tags in your mobile app, create a tagResolver object—a dictionary or map that pairs custom tags with the string values that will replace them when the paywall is rendered. Here's an example:
---
# File: using-custom-fonts-in-paywall-builder
---
---
title: "Custom fonts in Paywall Builder"
description: "Enhance Adapty’s Paywall Builder with custom fonts to improve design."
---
Consistency in visuals is key to great design. When building no-code paywalls, you might want to use a custom font to match the rest of your app. Here, we'll discuss how to customize fonts and how you can use them.
:::warning
This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Custom fonts in legacy Paywall Builder](using-custom-fonts-in-legacy-paywall-builder).
:::
## What can be customized
Every text element in Paywall Builder can have its own font and style. You can adjust this in the font controls for each text element:
In some cases, it’s more convenient to change the font for the entire paywall. You can do this in the Layout section of the Paywall Builder by adjusting the [by adjusting the Paywall Font](paywall-layout-and-products#font-settings-of-your-paywall).
## Fonts available by default
When you create a paywall in the Builder, Adapty uses a system font by default. This usually means SF Pro on iOS and Roboto on Android, though it can vary depending on the device. You can also choose from commonly used fonts like Arial, Times New Roman, Courier New, Georgia, Palatino, and Verdana. Each of these fonts comes with a few style options:
:::note
These fonts are not supplied as part of the Adapty SDK and are only used for preview purposes. We cannot guarantee they will work perfectly on all devices. However, in our testing, these fonts are typically recognized by most devices without any additional effort on your part. You can also [checkout which fonts are available by default on iOS](https://developer.apple.com/fonts/system-fonts/).
:::
## How to add a custom font to the Adapty Dashboard
If you need more than what’s offered by default, you can add a custom font. Once added, the custom font will be available throughout the app, and you can use it for any text line on any paywall.
1. Choose **Add custom font** in any of the font dropdowns:
2. In the **Add Custom Font** window:
1. Upload your font file (maximum size: 10MB).
2. Enter a name to reference it in the Paywall Builder.
3. Specify the correct font names for both platforms.
4. Ensure the font file is included in your app’s bundle if it hasn’t been added yet.
:::warning
The font file you upload is not sent to the device; it’s only used for preview purposes. Our SDK receives only the strings referencing the font to use while rendering the paywall. Therefore, you must include the same font file in the app bundle and provide the correct platform-specific font names for everything to work smoothly. Don’t worry, it won’t take much time.
:::
:::note
By uploading the font file to Adapty, you’re confirming that you have the right to use it in your app.
:::
## Getting the correct font name on iOS
There are two ways to get the correct ID for a font: the first involves some basic coding, and the second involves using an app called "Font Book," available on macOS.
If you’ve already added a custom font to your app’s bundle, you’re likely already referencing it by the font name. To confirm, call `UIFont.familyNames()` to get the family name of the font and then plug it into `UIFont.fontNames(forFamilyName: familyName)`. You can do this in `viewDidLoad` and then remove the code snippet:
```swift showLineNumbers title="Swift"
override func viewDidLoad() {
super.viewDidLoad()
...
for family in UIFont.familyNames.sorted() {
print("Family: \(family)")
let names = UIFont.fontNames(forFamilyName: family)
for fontName in names {
print("- \(fontName)")
}
}
}
```
The `fontName` in the above snippet is what you’re looking for. It might look something like "MyFont-Regular."
The second method is simpler: Install the font on your Mac, open the **Font Book** app, find the font, and use its `PostScript name`:
## Getting the correct font name on Android
If you’ve properly added the font file to the resource folder, simply provide the name of the file. Make sure it’s lowercase and contains only letters, numbers, and underscores—otherwise, it might not work.
You can confirm the filename is correct by calling `ResourcesCompat.getFont(context, R.font.my_font)`, with `my_font` being the filename you’re using. In this case, `my_font` is exactly what you should input when creating a custom font in Adapty.
## Adding the font files to your app's bundle
If you’re already using a custom font elsewhere in your app, you just need to add your paywall fonts in the same way. If not, make sure to include the font file in your app's project and bundle. Read how to do it below:
- On iOS: [In Apple official documentation](https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app)
- On Android: [In Android official documentation](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml)
:::important
When downloading font bundles from Adapty, you'll receive all font variations in an archive. Only add the specific font files your paywall uses to your app bundle to minimize app size. For example, if you only use `OpenSans-Regular.ttf` in your paywall, don't include `OpenSans-Bold.ttf` in your app bundle.
:::
---
# File: migration-to-new-paywall-builder
---
---
title: "Migration to new Paywall Builder"
description: "Migrate to the new Adapty Paywall Builder for an enhanced subscription flow."
---
We’re thrilled to introduce our [**New Paywall Builder**](adapty-paywall-builder) ! This advanced no-code tool is designed to make creating custom paywalls more intuitive and powerful than ever before, allowing you to craft beautiful, engaging paywalls with ease. No technical or design expertise required!
## Key Features of the New Paywall Builder
- **Expanded Template Selection**: Choose from a vast array of professionally designed templates to kickstart your paywall creation. These templates offer various styles and layouts to suit different needs and preferences.
- **Enhanced Flexibility**: Enjoy greater flexibility with the ability to use design layers and new elements such as carousels, cards, product list, and footer. These enhancements give you the creative freedom to build any type of paywall you envision.
- **Revamped Existing Elements**: Existing elements have been significantly improved, providing more options and capabilities to bring your paywall ideas to life.
## Parallel Paywall Builder Versions
Adapty offers two versions of the Paywall Builder simultaneously:
- **New Paywall Builder**: Located under the **Builder & Generator** tab of the paywall in the Adapty Dashboard, this version is the most recent and versatile. Paywalls created here require iOS, Android, and React Native SDKs version 3.0 or higher and Flutter and Unity SDKs version 3.3.0 or higher.
- **Legacy Paywall Builder**: Found under the **Legacy Builder** tab, this outdated version should only be used to support older app versions with SDKs below v3.x.x. We recommend avoiding it for new paywalls as it will be deprecated soon.
## Migrating Paywalls to the New Builder
Migrating a paywall from the legacy builder to the new builder will create a new version of your paywall in the **Builder & Generator** tab. This version can be edited using the new Paywall Builder and will display in apps with Adapty SDK v3.0 or later. The legacy version remains in the **Legacy Builder** tab and supports apps with SDK 2.x or earlier.
You’ll maintain paywalls in both formats separately, with changes in one format not affecting the other.
## Steps to Migrate a Paywall
To migrate a paywall to the new Paywall Builder:
1. Open the paywall you want to migrate.
2. Open the **Builder & Generator** tab.
3. Click **Migrate paywall**.
4. After the migration is done, review the result and make sure the paywall looks as it should. If not, correct it.
5. Click **Save**.
6. If there are some issues, they will be highlighted in red and you will see them at once. Fix them and save the paywall again.
You can migrate your paywalls one at a time to review and fix them as needed.
:::info
Please note that paywalls created in the new Paywall Builder will only appear in app versions with Adapty SDK v3.0 or later.
:::
## We Value Your Feedback
Your feedback is invaluable to us. If you encounter any issues or have suggestions for improvements, please reach out to us. We’re here to support you and enhance your experience with the new Paywall Builder.
📧 **Contact Us**: [Adapty Support](mailto:support@adapty.io)
Enjoy building with the new Paywall Builder, and take your monetization strategy to the next level with our enhanced tools and features!
---
# File: customize-paywall-with-remote-config
---
---
title: "Design paywall with remote config"
description: "Customize your paywall with remote config in Adapty for better targeting."
---
The Paywall Remote Config is a powerful tool that provides flexible configuration options. It allows the use of custom JSON payloads to tailor your paywalls precisely. With it, you can define various parameters such as titles, images, fonts, colors, and more.
3. Switch to the **Remote config** tab.
Remote config has 2 views:
- [Table](customize-paywall-with-remote-config#table-view-of-the-remote-config)
- [JSON](customize-paywall-with-remote-config#json-view-of-the-remote-config)
Both the **Table** and **JSON** views include the same configuration elements. The only distinction is a matter of preference, with the sole difference being that the table view offers a context menu, which can be helpful for correcting localization errors.
You can switch between views by clicking on the **Table** or **JSON** tab whenever necessary.
Whatever view you've chosen to customize your paywall, you can later access this data from SDK using the`remoteConfig` or `remoteConfigString` properties of `AdaptyPaywall`, and make some adjustments to your paywall. You can also programmatically update remote config values using the [server-side API](api-adapty/operations/updatePaywall) to dynamically modify paywall configurations without manual dashboard updates. Here are some examples of how you can use a remote config.
### Table view of the remote config
If it's not common for you to work with code and there is a need to correct some values of the JSON, Adapty has the **Table** view for you.
It is a copy of your JSON in the format of a table that is easy to read and understand. Color coding helps to recognize different data types.
To add a key, click the **Add row** button. We automatically check the values and types mapping and show an alert if your corrections may lead to an invalid JSON.
Additional row options are mostly useful for [paywall localisations](add-remote-config-locale):
Now it's time to [create a placement](create-placement) and add the paywall to it. After that, you can
2. Open the **Localization** menu to view all added locales. New locales will be pre-filled with values from the default language.
Now, you can translate the content manually, use AI, or export the localization file for external translators.
## Translating paywalls with AI
AI-powered translation is a quick and efficient way to localize your paywall.
We automatically detect which lines have never been translated or have changed in English since their last translation and mark them as needing an update. Lines that were already translated and haven't changed will keep their original translation and won’t be re-translated.
Rich text formatting (bold, italic, colored text, etc.) won’t be preserved in the translated version. Please adjust the translated text manually as needed.
1. Click the localization icon to select languages for translation:
- **In the language column header**: Translates all lines at once—ideal for initial translation or when updating the entire language.
- **In individual lines**: Translates specific lines independently—useful when making targeted changes without affecting other translations.
2. Click **AI Translate** to apply translations. The paywall lines will be translated and added to the table.
## Exporting localization files for external translation
You can export localization files to share with your translators and then import the translated results back into Adapty.
Exporting by the **Export** button creates individual `.csv` files for each language, bundled into a single archive. If you only need one file, you can export it directly from the language-specific menu.
Once you’ve received the translated files, use the **Import** button to upload them all at once or individually. Adapty will automatically validate the files to ensure they match the correct format and paywall configuration structure.
### Import file format
To ensure a successful import, the import file must meet the following requirements:
- **File Name and Extension:**
The file name must match the locale it represents and have a `.csv` extension. You can verify and copy the locale name in the Adapty Dashboard. If the name is not recognized, the import will fail.
- **Valid CSV:**
The file must be a valid CSV format. Invalid files will fail to import
- **Only Commas as Separators**:
Use commas as separators. Other separators will result in errors.
- **Header Line**:
The file must include a header line.
- **Correct Column Names:**
The column names must be **id** and **value**.
- **No Additional Entities:**
Ensure the file doesn’t include entities not present in the current paywall configuration. Extra entities will result in errors.
- **Partial import:**
The file can include all or just some entities from the current paywall configuration.
| **Issue** | **Solution** |
| ---------------------------------------------- | ------------------------------------------------------------ |
| **Imported .csv files are invalid** | Validate the file to ensure it adheres to CSV standards. Check for missing or extra commas, incorrect separators, missing header lines, and ensure the column names are **id** and **value**. |
| **Some of the languages are not in the table** | Ensure file names match the locale names exactly as shown in the localization table. If they don’t match, rename them accordingly. Also, verify the file’s content to ensure it relates to the paywall configuration. |
## Manual localization
Sometimes, you might want to tweak translations, add different images for specific locales, or even adjust remote configurations directly.
1. Choose the element you want to translate and type in a new value. You can update both **String** and **List** values or replace images with those better suited for the locale.
2. Take advantage of the context menu in the English locale to resolve localization issues efficiently:
- **Copy this value to all locales**: Overwrites any changes made in non-English locales for the selected row, replacing them with the value from the English locale.
- **Revert all row changes to original values**: Discards any changes made during the current session and restores the values to their last saved state.
After adding locales to a paywall, make sure to implement locale codes correctly in your app's code. See
:::note
Pay attention to the locale code (`en`, `fr` and `it` ). You'll need to pass it to the `getViewConfiguration` method of our SDK to get the correct localization.
You can learn more about it in
4. Click **Locales** and select the languages you want to support. Save your changes to add these locales to the paywall.
Now, you can translate the content manually, use AI, or export the localization file for external translators.
## Translate paywalls with AI
AI-powered translation is a quick and efficient way to localize your paywall.
You can translate both **String** and **List** values. By default, all lines are selected (highlighted in violet). Lines that have already been translated are marked in green and won’t be included in the new translation by default. Lines that are not selected or translated appear in gray.
1. Select the lines to translate. It's a good idea to uncheck lines with IDs, URLs, and variables to prevent AI from translating them.
2. Select the languages for translation.
3. Click **AI Translate** to apply translations. The selected lines will be translated and added to the paywall, with the translated lines marked green.
## Exporting localization files for external translation
While AI-powered localization is becoming a popular trend, you might prefer a more reliable method, like using professional human translators or a translation agency with a strong track record. If that’s the case, you can export localization files to share with your translators and then import the translated results back into Adapty.
Exporting by the **Export** button creates individual `.json` files for each language, bundled into a single archive. If you only need one file, you can export it directly from the language-specific menu.
Once you’ve received the translated files, use the **Import** button to upload them all at once or individually. Adapty will automatically validate the files to ensure they match the correct format.
### Import file format
To ensure a successful import, the import file must meet the following requirements:
- **File Name and Extension:**
The file name must match the locale it represents and have a `.json` extension. You can verify and copy the locale name in the Adapty Dashboard. If the name is not recognized, the import will fail.
- **Valid JSON:**
The file must be a valid JSON. If it is not, the import will fail.
## Manual localization
Sometimes, you might want to tweak translations, add different images for specific locales, or even adjust remote configurations directly.
1. Choose the element you want to translate and type in a new value. You can update both **String** and **List** values or replace images with those better suited for the locale.
2. Take advantage of the context menu in the English locale to resolve localization issues efficiently:
- **Copy this value to all locales**: Overwrites any changes made in non-English locales for the selected row, replacing them with the value from the English locale.
- **Revert all row changes to original values**: Discards any changes made during the current session and restores the values to their last saved state.
After adding locales to a paywall, make sure to implement locale codes correctly in your app's code. See
3. ⚠️ If you choose Stripe, make sure you're using keys from the **Test Mode** environment despite the interface saying **Sandbox**. Otherwise your web paywall will not work. **Sandboxes** in Stripe are not yet supported.
:::important
To be able to use Apple Pay with Stripe, you need to verify the paywall domains in the Stripe settings:
1. Go to [Payment method domain settings](https://dashboard.stripe.com/settings/payment_method_domains) and click **Add a new domain**.
2. Add `app.funnelfox.com` and your personal paywall subdomain (it will look like `paywalls-....fnlfx.com`). To find your subdomain, on the web paywall creation page, go to **Settings > Domains** and copy the **Hosted subdomain** value.
To use Apple Pay with Paddle, verify the paywall domains in the Paddle settings:
1. In the Paddle console, go to **Checkout > Website approval** and click **Add a new domain**.
2. Add `app.funnelfox.com` and your personal paywall subdomain (it will look like `paywalls-....fnlfx.com`). To find your subdomain, on the web paywall creation page, go to **Settings > Domains** and copy the **Hosted subdomain** value.
The approval process in Paddle is manual, so you will need to wait until the domains move from `Pending` to `Approved`.
:::
## Create and configure a web paywall
1. On the web paywall list page, click **Create a paywall**.
2. Enter a paywall name and click **Create**.
3. You will be redirected to a basic template with two subscription options and the Apple Pay purchase button.
The first screen lists the subscription plans. The second and third screens are checkout screens. Each screen corresponds to one plan you offer. If you have only one plan, delete the extra screen. If you have more, you need to duplicate the checkout screens.
The last screen users see after a successful purchase is where you need to clearly indicate that they can return to your app.
4. Set up the plan list: add or remove plans and prices. All the prices and plans you see on the screen are not added dynamically, so you need to configure them manually.
5. Add or configure a checkout screen for each plan you have. We recommend adding a total amount to each checkout screen so users know how much they need to pay before they click the purchase button.
6. On checkout screens, you already have the Apple Pay button. For it to work, on each screen, configure:
1. **Product type**: Select whether you want to add a trial period or a discount.
2. **Trial period**: Enter the trial period duration.
3. **Product**: Select your product from your payment provider.
:::important
Ensure that the product is added to Adapty. Otherwise, the purchase result will be set to default.
:::
4. **Subscription discount**: Optionally, select a coupon from your payment provider.
7. Now, you need to associate plans with checkout screens. On the plan selection screen, click the **Continue** button and select a destination screen for each plan.
When you are ready with the paywall, you need to get its link to activate this paywall in Adapty. The way you get it depends on whether you are testing it or launching it in the production environment:
1. **For sandbox testing**: Click **Preview** on the top right and copy the link.
2. **For production**: Click **Publish** on the top right. Click **Home** and copy the link from the **URL** column.
That's it! Use this link to [proceed with the setup](web-paywall.md#step-2-activate-the-paywall).
---
# File: fallback-paywalls
---
---
title: "Fallback paywalls"
description: "Use fallback paywalls to ensure seamless user experience in Adapty."
---
To maintain a fluid user experience, it is important that you set up **fallback versions** for your [paywalls](paywalls) and [onboardings](onboardings).
When your application loads a paywall, the Adapty SDK requests paywall configuration data from our servers. But what if the device cannot connect to Adapty due to network issues or server outages?
* If the user accessed the paywall before, and the device cached its data, the application loads paywall data **from cache**.
* If the device did not cache the paywall, the application looks for a locally stored configuration file. It allows the application to display the paywall without an error.
Adapty automatically generates fallback configuration files for you to download and use. Each file contains platform-specific configurations for *all* your placements.
## Get started
1. [Download the fallback configuration file](/local-fallback-paywalls) from Adapty.
2. Use the Adapty SDK to configure your fallback paywalls:
* [iOS](ios-use-fallback-paywalls)
* [Android](android-use-fallback-paywalls)
* [React Native](react-native-use-fallback-paywalls)
* [Flutter](flutter-use-fallback-paywalls)
* [Unity](unity-use-fallback-paywalls)
* [Kotlin Multiplatform](kmp-use-fallback-paywalls)
* [Capacitor](capacitor-use-fallback-paywalls)
## Limitations
Fallback paywalls are hard-coded and locally stored, so they lack the dynamic capabilities of regular Adapty paywalls.
* Fallback paywalls don't support [internationalization](paywall-localization). When Adapty generates the configuration file, it uses the default `en` locale.
* Each placement can only have one fallback paywall. If your setup inlcudes different paywall configurations for different [audiences](audience), Adapty uses the configuration intended for "All users".
* Fallback paywalls don't support [A/B testing](ab-tests). If a paywall participates in an A/B test, its fallback configuration file will include the variation with the highest weight.
* Fallback paywalls cannot be [managed remotely](customize-paywall-with-remote-config). If you want to update the configuration file, you need to release a new version of the app on App Store / Google Play.
---
# File: local-fallback-paywalls
---
---
title: "Download fallback paywalls"
description: "Use local fallback paywalls in Adapty to ensure seamless subscription flows."
---
Adapty automatically generates JSON configuration files for your [fallback paywalls](/fallback-paywalls), one per platform. These files contain fallback data for your [onboardings](local-fallback-onboarding), as well.
If a single placement has more than one paywall or onboarding, the fallback version will include the variation with the highest weight, or the widest audience. Adapty updates these files whenever you modify your paywalls or onboardings.
Follow the steps below to download your fallback configurations:
1. Open the **[Placements](https://app.adapty.io/placements)** page.
2. Click the **Fallbacks** button.
3. Select your target platform (*iOS* or *Android*) from the dropdown.
4. Select your SDK version to start the download.
## After the download
Follow the setup guide for your particular platform:
* [iOS](ios-use-fallback-paywalls)
* [Android](android-use-fallback-paywalls)
* [React Native](react-native-use-fallback-paywalls)
* [Flutter](flutter-use-fallback-paywalls)
* [Unity](unity-use-fallback-paywalls)
* [Kotlin Multiplatform](kmp-use-fallback-paywalls)
* [Capacitor](capacitor-use-fallback-paywalls)
---
# File: paywall-metrics
---
---
title: "Paywall metrics"
description: "Track and analyze paywall performance metrics to improve subscription revenue."
---
Adapty collects a series of metrics to help you better measure the performance of the paywalls. All metrics are updated in real-time, except for the views, which are updated once every several minutes. All metrics, except for the views, are attributed to the product within the paywall. This document outlines the metrics available, their definitions, and how they are calculated.
Paywall metrics are available on the paywall list, providing you with an overview of the performance of all your paywalls. This consolidated view presents aggregated metrics for each paywall, allowing you to assess their effectiveness and identify areas for improvement.
For a more granular analysis of each paywall, you can navigate to the paywall detail metrics. In this section, you will find comprehensive metrics specific to the selected paywall, offering deeper insights into its performance.
### Metrics controls
The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with three levels of indentation.
For Live paywall, the metrics cover the period from the paywall's start date until the current date. For inactive paywalls, the metrics encompass the entire period from the start date to the end of the selected time period. Draft and archived paywalls are included in the metrics table, but if there is no data available for those paywalls, they will be listed without any displayed metrics.
#### View options for metrics data
The paywall page offers two view options for metrics data: placement-based and audience-based.
In the placement-based view, metrics are grouped by placements associated with the paywall. This allows users to analyze metrics by different placements.
In the audience-based view, metrics are grouped by the target audience of the paywall. Users can assess metrics specific to different audience segments. You can select the preferred view using the dropdown option at the top of the paywall detail page.
#### Profile install date filtration
The Filter metrics by install date checkbox enables the filtering of metrics based on the profile install date, instead of the default filters that use trial/purchase date for transactions or view date for paywall views. By selecting this checkbox, you can focus on measuring user acquisition performance for a specific period by aligning metrics with the profile install date. This option is useful for customizing the metrics analysis according to your specific needs.
#### Time ranges
You can choose from a range of time periods to analyze metrics data, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges.
#### Available filters and grouping
Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. With Adapty's metrics page, you have access to various time ranges, grouping options, and filtering possibilities.
- Filter by: Audience, country, paywall, paywall state, paywall group, placement, country, store, product, and product store.
- Group by: Product and store.
You can find more information about the available controls, filters, grouping options, and how to use them for paywall analytics in [this documentation.](controls-filters-grouping-compare-proceeds)
#### Single metrics chart
One of the key components of the paywall metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis.
The chart section on the paywall metrics page includes a horizontal bar chart that visually represents the chosen metric values. Each bar in the chart corresponds to a metric value and is proportional in size, making it easy to understand the data at a glance. The horizontal line indicates the timeframe being analyzed, and the vertical column displays the numeric values of the metrics. The total value of all the metric values is displayed next to the chart.
Additionally, clicking on the arrow icon in the top right corner of the chart section expands the view, displaying the selected metrics on the full line of the chart.
#### Total metrics summary
Next to the single metrics chart, the total metrics summary section is shown, which displays the cumulative values for the selected metrics at a specific point in time, with the ability for you to change the displayed metric using a dropdown menu.
### Metrics definitions
#### Revenue
This metric represents the total amount of money generated in USD from purchases and renewals. Please note that the revenue calculation does not include the App Store / Play Store commission and is calculated before deducting any fees.
#### Proceeds
This metric represents the actual amount of money received by the app owner in USD from purchases and renewals after deducting the applicable App Store / Play Store commission.
:::important
Notify Adapty if your app is enrolled in a reduced commission program. To ensure correct calculations, specify your [Small Business Program](app-store-small-business-program) and [Reduced Service Fee program](google-reduced-service-fee) status in your [app settings](general).
:::
It reflects the net revenue that directly contributes to the app's earnings. For more information on how proceeds are calculated, you can refer to the Adapty [documentation.](analytics-cohorts#revenue-vs-proceeds)
#### ARPPU
ARPPU is an average revenue per paying user. It’s calculated as total revenue divided by the number of unique paying users. $15000 revenue / 1000 paying users = $15 ARPPU.
#### ARPAS
The average revenue per active subscriber allows you to measure the average revenue generated per active subscriber. It is calculated by dividing the total revenue by the number of subscribers who have activated a trial or subscription. For example, if the total revenue is $5,000 and there are 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber.
#### Unique conversion rate (CR) to purchases
The unique conversion rate to purchases is calculated by dividing the number of purchases by the number of unique views. For example, if there are 10 purchases and 100 unique views, the unique conversion rate to purchases would be 10%. This metric focuses on the ratio of purchases to the unique number of views, providing insights into the effectiveness of converting unique visitors into paying customers.
#### CR to purchases
The conversion rate to purchases is calculated by dividing the number of purchases by the total number of views. For example, if there are 10 purchases and 100 views, the conversion rate to purchases would be 10%. This metric indicates the percentage of views that result in purchases, providing insights into the effectiveness of your paywall in converting users into paying customers.
#### Unique CR to trials
The unique conversion rate to trials is calculated by dividing the number of trials started by the number of unique views. For example, if there are 30 trials started and 100 unique views, the unique conversion rate to trials would be 30%. This metric measures the percentage of unique views that result in trial activations, providing insights into the effectiveness of your paywall in converting unique visitors into trial users.
#### Purchases
Purchases represent the cumulative total of various transactions made on the paywall. The following transactions are included in this metric (renewals are not included):
- New purchases, that are made directly on the paywall.
- Trial conversions of trials that were initially activated on the paywall.
- Downgrades, upgrades, and cross-grades of subscriptions made on the paywall.
- Subscription restores on the paywall, such as when a subscription is reinstated after expiration without auto-renewal.
By considering these different types of transactions, the purchases metric provides a comprehensive view of the overall acquisition and monetization activity on your paywall.
#### Trials
The trials metric represents the total number of trials that have been activated. It reflects the number of users who have initiated trial periods through your paywall. This metric helps track the effectiveness of your trial offering and can provide insights into user engagement and conversion from trials to paid subscriptions.
#### Trials canceled
The trials canceled metric represents the number of trials in which the auto-renewal feature has been switched off. This occurs when users manually unsubscribe from the trial, indicating their decision not to continue with the subscription after the trial period ends. Tracking trials canceled provides valuable information about user behavior and allows you to understand the rate at which users opt out of the trial.
#### Refunds
The refunds metric represents the number of refunded purchases and subscriptions. This includes transactions that have been reversed or refunded due to various reasons, such as customer requests, payment issues, or any other applicable refund policies.
#### Refund rate
The refund rate is calculated by dividing the number of refunds by the number of first-time purchases (renewals are not included). For example, if there are 5 refunds and 1000 first-time purchases, the refund rate would be 0.5%.
#### Views
The views metric represents the total number of times the paywall has been viewed by users. Each time a user visits the paywall, it is counted as a separate view. For example, if a user visits the paywall two times, it will be recorded as two views. Tracking views helps you understand the level of engagement and user interaction with your paywall, providing insights into user behavior and the effectiveness of your paywall placement and design.
#### Unique views
The unique views metric represents the number of unique instances in which the paywall has been viewed by users. Unlike total views, which count each visit as a separate view, unique views count each user's visit to the paywall only once, regardless of how many times they access it. For example, if a user visits the paywall two times, it will be recorded as one unique view. Tracking unique views helps provide a more accurate measure of user engagement and the reach of your paywall, as it focuses on individual users rather than the total number of visits.
:::warning
Make sure to send paywall views to Adapty using `.logShowPaywall()` method. Otherwise, paywall views will not be accounted for in the metrics and conversions will be irrelevant.
:::
---
# File: migrate-paywalls
---
---
title: "Migrate paywalls between apps"
description: "Learn how to migrate paywalls from other apps in Adapty."
---
With Adapty, you don't need to build a new paywall from scratch for every app. If you manage multiple apps, you can migrate the paywall builder configuration of any paywall created with the builder from one app to another.
Migration lets you copy all visual configurations:
- Layout settings for paywall and all paywall elements
- Media
- Localization
Migration applies only to builder configuration, and it doesn't copy products or remote config.
:::note
If you migrate a paywall builder configuration with custom fonts, test them on a device as they may display incorrectly.
:::
## Migrate paywall
:::important
You can migrate only paywalls created in the **new** Adapty paywall builder. To migrate **legacy** paywall builder paywalls, you must migrate them to the new paywall builder first.
:::
To migrate a paywall builder configuration:
1. **For new paywall**: Start [paywall creation](create-paywall.md) and add products. Then, click **Build no-code paywall** to open the template library.
**For existing paywall**: Go to the **Layout settings** section of the **Builder & Generator** tab and click **Change template**.
2. Click **Choose paywall** inside the **Copy a design from your apps** box when editing the paywall template.
3. Select the app and the paywall you want to copy the configuration from.
4. Click **Copy Selected Paywall**.
After migration, you can make any edits you need and they won't affect the original paywall.
---
# File: duplicate-paywalls
---
---
title: "Duplicate paywall"
description: "Learn how to manage duplicate paywalls and optimize paywall performance in Adapty."
---
If you need to make small changes to an existing paywall in Adapty, especially when it's already being used in your mobile app and you don't want to mess up your analytics, you can simply duplicate it. You can then use these duplicates to replace the original paywalls in some or all placements as needed.
This creates a copy of the paywall with all its details, like its name, products, and any promotions. The new paywall will have "Copy" added to its name so you can easily tell it apart from the original.
To duplicate a paywall in the Adapty dashboard:
1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu. The paywall list page in the Adapy dashboard provides an overview of all the paywalls present in your account.
2. Click the **3-dot** button next to the paywall and select the **Duplicate** option.
3. Adjust the new paywall and click the **Save** button.
4. Adapty will prompt you to replace the original paywalls with their duplicates in placements if the original paywall is currently used in any placement. If you choose **Create and replace original**, the new paywalls will immediately go **Live**. Alternatively, you can create them as new paywalls in the **Draft** state and add them to placements later.
---
# File: archive-paywalls
---
---
title: "Archive paywall"
description: "Learn how to archive outdated paywalls in Adapty without losing data."
---
As you dive into Adapty and fine-tune your paywall settings, you might accumulate paywalls that no longer fit your current strategy or campaigns. These unused paywalls, left `Inactive`, can clutter your workspace, making it tricky to find the ones that matter most. To tackle this, Adapty introduces the option to archive these unnecessary paywalls.
Archiving ensures they're safely stored without permanent deletion, ready to be accessed if needed in the future. Plus, archived paywalls can be filtered out from the default view, decluttering your workspace and simplifying your user interface. In this guide, we'll walk you through efficiently archiving paywalls in Adapty, giving you greater control over your paywall management process.
Just a friendly reminder: Live paywalls that are currently active in at least one placement cannot be archived. If you wish to archive such a paywall, simply remove it from all placements beforehand.
:::note
You can't archive a paywall if it is used in a non-archived A/B test. This way, the user can view detailed metrics for a completed A/B test, and the linked paywall is part of that data.
:::
**To archive a paywall:**
1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu.
2. Click the **3-dot** button next to the paywall and select the **Archive** option.
3. When you're in the **Archive paywall** window, simply type in the name of the paywall you wish to archive and then click the **Archive** button.
---
# File: restore-paywall
---
---
title: "Return paywall from archive"
description: "Restore paywalls in Adapty to ensure uninterrupted subscription services for users."
---
Having the ability to archive paywalls is a highly beneficial feature for streamlining your paywall management process. It allows you to conceal paywalls that are no longer needed, reducing clutter in your workspace. Moreover, the option to restore archived paywalls provides flexibility, enabling you to reintroduce them into your strategy if they prove to be useful again.
Archived paywalls may be filtered out of the default view. To see them, select **Archived** in the **State** filter.
**To return a paywall back from the archive**
1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu.
2. Make sure that archived paywalls are displayed in the list. If not, update the filter on the right.
3. Click the **3-dot** button next to the archived paywall and select **Back to active**.
---
# File: choose-meaningful-placements
---
---
title: "Choose meaningful placements"
description: "Optimize paywall placements with Adapty for increased user engagement and revenue."
---
When [creating placements](create-placement), it's essential to consider the logical flow of your app and the user experience you want to create. Most apps should have no more than 5 [placements](placements) without sacrificing the ability to run experiments. Here's an example of how you can structure your placements:
1. **Onboarding flow:** This stage represents the first interaction your users have with your app. It's an excellent opportunity to introduce your users to your app's value proposition by using both onboarding and paywall placements here. Over 80% of subscriptions are activated during onboarding flow, so it's important to focus on selling the most profitable subscriptions here. With Adapty, you can easily have different [onboardings](https://adapty.io/docs/onboardings) and [paywalls](https://adapty.io/docs/paywalls) for different audiences, and run A/B tests to find the best option for your app. For example, you can run an A/B test for users from the US, showing more expensive subscriptions 50% of the time.
2. **App settings:** If the user hasn't subscribed during the onboarding flow, you can create a paywall placement within your app. This can be in the app settings or after the user has completed a specific target action. Since users inside the app tend to think more thoroughly about subscribing, the products on this paywall might be slightly less expensive compared to those in the onboarding stage.
3. **Promo:** If the user hasn't subscribed after seeing the paywall multiple times, it could indicate that the prices are too high for them or they are hesitant about subscriptions. In this case, you can show a special offer to them with the most affordable subscription or even a lifetime product. This can help entice users who are price-sensitive or skeptical about subscriptions to make a purchase.
Most apps will have similar logic and placements, following the user journey and key points where paywalls, onboardings, or A/B tests can be displayed to drive conversions and revenue. You can configure them in each placement to experiment and optimize your monetization strategies.
---
# File: create-placement
---
---
title: "Create placement"
description: "Create and manage placements in Adapty to improve paywall performance."
---
A [Placement](placements) is a specific location in your mobile app where you can show a paywall, onboarding, or A/B test. For example, a subscription choice might appear in a startup flow, while a consumable product (like golden coins) could show when a user runs out of coins in a game.
You can show the same or different paywalls, onboardings, or A/B tests in various placements or to different user segments — called "audiences" in Adapty.
Read the [Choose meaningful placements](choose-meaningful-placements) section for tips on picking the right placement.
:::info
Although the placement creation process is similar for paywalls and onboardings, you can't create one placement for both as they process different metrics.
:::
## Create and configure a placement
1. Go to **[Placements](https://app.adapty.io/placements)** from the Adapty main menu.
2. Click **Create placement**.
3. Enter a **Placement name**. This is an internal identifier in the Adapty Dashboard. You can edit it later if needed.
4. Enter a **Placement ID**. You'll use this ID in the Adapty SDK to call the placement's [paywalls](paywalls), [onboardings](onboardings), and [A/B tests](ab-tests). You cannot edit it later as it's unique for each placement.
Next, assign a paywall, onboarding, or A/B test to the placement. Adapty supports [audiences](audience) — user segments based on [segments](segments.md) — so you can show different content to different user groups. If you don't need targeting, the default *All users* audience covers everyone.
:::note
To proceed, ensure that you created a paywall, onboarding, or A/B test you want to run and an audience, you'd like to specify.
:::
1. In the **Placements/ Your placement** window, add a paywall, onboarding, or A/B test to display for default *All users* audience. To do this, click either the **Run paywall** or **Run A/B test** button, then select the desired paywall, onboarding, or A/B test from the dropdown list.
2. If you want to use more than one audience in the placement to create personalized paywalls tailored to different user groups, click the **Add audience** button and choose the desired user segment from the list.
The exported CSV file contains the following information about your placements:
- Placement ID
- Placement name
- Audience name
- Segment name
- Cross-placement A/B test name
- A/B test name
- Paywall name
---
# File: delete-placement
---
---
title: "Delete placement"
description: "Find out how to delete a placement in Adapty without affecting your paywall performance."
---
A [Placement](placements) designates a specific location within your mobile app where a paywall, onboarding, or A/B test can be displayed.
:::danger
Although you have the option to delete any placement, it is critical to ensure that you don't delete a placement that is actively used in your mobile app. Deleting an active paywall placement will result in a local fallback paywall being permanently shown if you've [set it up](fallback-paywalls), and you won't be able to ever replace it with a dynamic paywall in released app versions.
:::
To delete an existing placement:
1. Go to **[Placements](https://app.adapty.io/placements)** from the Adapty main menu. If you want to delete a placement for onboarding, switch to the **Onboardings** tab.
2. Click the **3-dot** button next to the placement and select the **Delete** option.
3. In the opened **Delete placement** window, enter the product name you're about to delete.
4. Click the **Delete forever** button to confirm the deletion.
---
# File: add-audience-paywall-ab-test
---
---
title: "Add audience and paywall or A/B test to placement"
description: "Run A/B tests on paywalls for different audience segments in Adapty."
---
**Audiences** in Adapty are groups of users defined by [segments](segments). They let you show paywalls, onboardings, and A/B tests to the users who should see them. Build segments with filters to ensure each group gets the right content.
When you add an audience to a [placement](placements), you target paywalls, onboardings, or A/B tests at a specific user group. Linking an audience to a placement makes sure the right users see the right content at the right moment in their app journey.
Open the placement where you want to add a paywall, onboarding, or A/B test, or create a new one in the [Placements](https://app.adapty.io/placements) menu.
:::note
To proceed, ensure that you created a paywall, onboarding, or A/B test you want to run and an audience, you'd like to specify.
:::
1. In the **Placements/ Your placement** window, add a paywall, onboarding, or A/B test to display for default *All users* audience. To do this, click either the **Run paywall** or **Run A/B test** button, then select the desired paywall, onboarding, or A/B test from the dropdown list.
2. If you want to use more than one audience in the placement to create personalized paywalls tailored to different user groups, click the **Add audience** button and choose the desired user segment from the list.
In this scenario, we rely on audience priority. Audience priority is a numerical order, where #1 is the highest. It guides the sequence for audiences to check. In simpler terms, audience priority helps Adapty make decisions about which audience to apply first when selecting the paywall, onboarding, or A/B test to display. If the audience priority is low, users who potentially qualify might be bypassed. Instead, they could be directed to another audience with a higher priority.
Crossplacement audiences, meaning those created for [crossplacement A/B tests](ab-tests#ab-test-types), always take priority over regular audiences.
The "All users" audience always has the lowest priority since it’s a fallback and includes everyone who doesn’t match any other audience.
To adjust audience priorities for a placement:
1. While creating a new or editing an existing placement, click **Edit priority**. The button is visible only if at least three audiences are added to a placement ("All users" and two others). If less, the order is obvious - the "All users" audience comes last.
2. In the opened **Edit audience priorities** window, drag-and-drop audiences to reorder them correctly.
3. Click the **Save** button.
---
# File: placement-metrics
---
---
title: "Placement metrics"
description: "Analyze placement metrics in Adapty to improve paywall performance."
---
With Adapty, you have the flexibility to create and manage multiple placements in your app, each associated with distinct paywalls or A/B tests. This versatility enables you to target specific user segments, experiment with different offers or pricing models, and optimize your app's monetization strategy.
To gather valuable insights into the performance of your placements and user engagement with your offers, Adapty tracks various user interactions and transactions related to the displayed paywalls. The robust analytics system captures metrics including views, unique views, purchases, trials, refunds, conversion rates, and revenue.
The collected metrics are continuously updated in real-time and can be conveniently accessed and analyzed through Adapty's user-friendly dashboard. You have the freedom to customize the time range for analysis, apply filters based on different parameters, and compare metrics across various placements, user segments, or products.
Placement metrics are available on the placements list, where you can get an overview of the performance of all your placements. This high-level view provides aggregated metrics for each placement, allowing you to compare their performance and identify trends.
For a more detailed analysis of each placements, you can navigate to the placements detail metrics. On this page, you will find comprehensive metrics specific to the selected placements. These metrics provide deeper insights into how a particular placements is performing, allowing you to assess its effectiveness and make data-driven decisions.
### Metrics controls
The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with four levels of indentation.
#### View options for metrics data
The placement metrics page offers two view options for metrics data: paywall-based and audience-based.
In the paywall-based view, metrics are grouped by placements associated with the paywall. This allows users to analyze metrics by different placements.
In the audience-based view, metrics are grouped by the target audience of the paywall. Users can assess metrics specific to different audience segments.
#### Profile install date filtration
#### Time ranges
You can choose from a range of time periods to analyze metrics data, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges.
#### Available filters and grouping
Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. With Adapty's metrics page, you have access to various time ranges, grouping options, and filtering possibilities.
- ✅ Filter by: Audience, paywall, paywall group, placement, country, store.
- ✅ Group by: Segment, store, and product
You can find more information about the available controls, filters, grouping options, and how to use them for paywall analytics in [this documentation.](controls-filters-grouping-compare-proceeds)
#### Single metrics chart
One of the key components of the placement metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis.
The chart section on the placements metrics page includes a horizontal bar chart that visually represents the chosen metric values. Each bar in the chart corresponds to a metric value and is proportional in size, making it easy to understand the data at a glance. The horizontal line indicates the timeframe being analyzed, and the vertical column displays the numeric values of the metrics. The total value of all the metric values is displayed next to the chart.
Additionally, clicking on the arrow icon in the top right corner of the chart section expands the view, displaying the selected metrics on the full line of the chart.
#### Total metrics summary
Next to the single metrics chart, the total metrics summary section is shown, which displays the cumulative values for the selected metrics at a specific point in time, with the ability for you to change the displayed metric using a dropdown menu.
### Metrics definitions
Unlock the power of placement metrics with our comprehensive definitions. From revenue to conversion rates, gain valuable insights that will supercharge your monetization strategies and drive success for your app.
#### Revenue
This metric represents the total amount of money generated in USD from purchases and renewals within specific placements. Please note that the revenue calculation does not include the Apple App Store or Google Play Store commission and is calculated before deducting any fees.
#### Proceeds
This metric represents the actual amount of money received by the app owner in USD from purchases and renewals within specific placements after deducting the applicable Apple App Store or Google Play Store commission. It reflects the net revenue that directly contributes to the app's earnings. For more information on how proceeds are calculated, you can refer to the Adapty [documentation.](analytics-cohorts#revenue-vs-proceeds)
#### ARPPU
ARPPU stands for Average revenue per paying user and measures the average revenue generated per paying user within specific placements. It is calculated as the total revenue divided by the number of unique paying users. For example, if the total revenue is $15,000 and there are 1,000 paying users, the ARPPU would be $15.
#### ARPAS
ARPAS, or Average revenue per active subscriber, allows you to measure the average revenue generated per active subscriber within specific placements. It is calculated by dividing the total revenue by the number of subscribers who have activated a trial or subscription. For example, if the total revenue is $5,000 and there are 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber.
#### ARPU
For onboarding placements only. ARPU is the average revenue per user who viewed the onboarding. It's calculated as total revenue divided by the number of unique viewers.
#### Unique CR to purchases
The Unique conversion rate to purchases is calculated by dividing the number of purchases within specific placements by the number of unique views. It focuses on the ratio of purchases to the unique number of views, providing insights into the effectiveness of converting unique visitors within specific placements into paying customers.
#### CR to purchases
The Conversion rate to purchases is calculated by dividing the number of purchases within specific placements by the total number of views of paywalls. It indicates the percentage of views within specific placements that result in purchases, providing insights into the effectiveness of your paywall in converting users into paying customers.
#### Unique CR to trials
The unique conversion rate to trials is calculated by dividing the number of trials started within specific placements by the number of unique views. It measures the percentage of unique views within specific placements that result in trial activations, providing insights into the effectiveness of your paywall in converting unique visitors into trial users.
#### Purchases
Purchases represent the cumulative total of various transactions made on the paywall within specific placements. The following transactions are included in this metric (renewals are not included):
- New purchases are made directly within specific placements.
- Trial conversions of trials that were initially activated within specific placements.
- Downgrades, upgrades, and cross-grades of subscriptions made within specific placements.
- Subscription restores within specific placements, such as when a subscription is reinstated after expiration without auto-renewal.
By considering these different types of transactions, the purchases metric provides a comprehensive view of the overall acquisition and monetization activity within specific placements.
#### Trials
The trials metric represents the total number of trials that have been activated within specific placements. It reflects the number of users who have initiated trial periods through your paywall within those placements. This metric helps track the effectiveness of your trial offering and can provide insights into user engagement and conversion from trials to paid subscriptions.
#### Trials canceled
The trials canceled metric represents the number of trials within specific placements in which the auto-renewal feature has been switched off. This occurs when users manually unsubscribe from the trial, indicating their decision not to continue with the subscription after the trial period ends. Tracking trials canceled provides valuable information about user behavior and allows you to understand the rate at which users opt out of the trial within specific placements.
#### Refunds
The refunds metric represents the number of refunded purchases and subscriptions within specific placements. This includes transactions that have been reversed or refunded due to various reasons, such as customer requests, payment issues, or any other applicable refund policies.
#### Refund rate
The refund rate is calculated by dividing the number of refunds within specific placements by the number of first-time purchases (renewals are not included). For example, if there are 5 refunds and 1,000 first-time purchases, the refund rate would be 0.5%.
#### Views
The views metric represents the total number of times the paywall within specific placements has been viewed by users. Each time a user visits the paywall within those placements, it is counted as a separate view. Tracking views helps you understand the level of engagement and user interaction with your paywall, providing insights into user behavior and the effectiveness of your paywall placement and design within specific areas of your app.
#### Unique views
The unique views metric represents the number of unique instances in which the paywall within specific placements has been viewed by users. Unlike total views, which count each visit as a separate view, unique views count each user's visit to the paywall within those placements only once, regardless of how many times they access it. Tracking unique views helps provide a more accurate measure of user engagement and the reach of your paywall within specific placements, as it focuses on individual users rather than the total number of visits.
#### Completions & unique completions
For onboarding placements only. Completions count the number of times users complete your onboarding placement, meaning that they go from the first to the last screen. If someone completes it twice, that's two **completions** but one **unique completion**.
#### Unique completions rate
For onboarding placements only. The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding placement and make changes if you notice that people ignore it.
---
# File: create-access-level
---
---
title: "Create access level"
description: "Create and assign access levels in Adapty for better user segmentation."
---
Access levels let you control what your app's users can do in your mobile app without hardcoding specific product IDs. Each product defines how long the user gets a certain access level for. So, whenever a user makes a purchase, Adapty grants access to the app for a specific period (for subscriptions) or forever (for lifetime purchases).
When you create an app in the Adapty Dashboard, the `premium` access level is automatically generated. This serves as the default access level and it cannot be deleted.
To create a new access level:
1. Go to **[Products](https://app.adapty.io/access-levels)** from the Adapty main menu, then select the **Access levels** tab.
2. Click **Create access level**.
3. In the **Create access level** window, assign it an ID. This ID will serve as the identifier within your mobile app, enabling access to additional features upon user purchase. Additionally, this identifier aids in distinguishing one access level from others within the app. Ensure it's clear and easily understandable for your convenience.
4. Click **Create access level** to confirm the access level creation.
---
# File: assigning-access-level-to-a-product
---
---
title: "Assign access level to product"
description: "Assign access levels to products to optimize subscription management."
---
Every [Product](product) requires an associated access level to ensure that users receive the corresponding gated content upon purchase. Adapty seamlessly determines the subscription duration, which then serves as the expiration date for the access level. In the case of a lifetime product, if a customer purchases it, the access level remains perpetually active without any expiration date.
To link an access level to a product:
1. While [configuring a product](create-product), select the access level from the **Access Level ID** list.
2. Click **Save**.
---
# File: give-access-level-to-specific-customer
---
---
title: "Give access level to specific customer"
description: "Assign specific access levels to customers using Adapty’s advanced tools."
---
You can manually adjust the access level for a particular customer right in the Adapty Dashboard. This is useful, especially in support scenarios. For example, if you'd like to extend a user's premium usage by an extra week as a thank-you for leaving a fantastic review.
## Give access level to a specific customer in the Adapty Dashboard
1. Go to **[Profiles and Segments](https://app.adapty.io/placements)** from the Adapty main menu.
2. Click on the customer you want to grant access to.
3. Click **Add access level**.
4. Select the access level to grant and when it should expire for this customer.
5. Click **Apply**.
## Give access level to a specific customer via API
You also have the option to give a customer an access level from your server using the Adapty API. This comes in handy if you have bonuses for referrals or other events related to your products. Find additional details on the [Grant access level with server-side API](api-adapty/operations/grantAccessLevel) page.
---
# File: local-access-levels
---
---
title: "Local access levels"
description: "Manage access levels in case of temporary outages."
---
:::important
Note the following:
- Local access levels are supported in the Adapty SDK starting from version 3.12.
- By default, local access levels are disabled on Android for additional security. If you need them, enable them during the SDK activation: [Android](sdk-installation-android#enable-local-access-levels), [React Native](sdk-installation-reactnative#enable-local-access-levels-android), [Flutter](sdk-installation-flutter#enable-local-access-levels).
:::
Each product you configure has an [**access level**](access-level.md) linked to it. When your users make a purchase, the Adapty SDK assigns the access level to the user [profile](profiles-crm.md), so you need to use this access level to determine whether users can access paid content in the app.
The Adapty SDK is very reliable, and it's very rare for its servers to be unavailable. However, even in this rare case, your users won't notice it.
If a user makes a purchase, but Adapty cannot receive a response, the SDK switches to verifying purchases directly in the store. Therefore, the access level is granted locally in the app, and no additional setup is required to enable it. The SDK handles this automatically behind the scenes, and users will access what they paid for just like normal.
Note the following about how local access levels work:
- When users are back online, transaction information is automatically pushed to the Adapty servers, which then applies the transactions to the user profile and returns the updated profile to the SDK.
- Updated data won't appear in the Adapty analytics until data is pushed.
- Local access levels work only when the Adapty servers are down. Otherwise, the SDK will use any cached data.
- Local access levels don't work for consumable products, except when a consumable product is assigned a subscription type (monthly, annual, weekly, etc.) in the Adapty dashboard.
---
# File: create-onboarding
---
---
title: "Create onboarding"
---
[Onboardings](onboardings.md) introduce new users to your mobile app's value, features, and usage tips.
## Step 1. Create an onboarding
To create a new onboarding in the Adapty dashboard:
1. Go to **Onboardings** from the Adapty main menu. This page gives an overview of all onboardings you’ve set up, along with their metrics. Click **Create onboarding**.
2. Create a descriptive name for your onboarding and click **Proceed to build onboarding**.
3. You will be redirected to the onboarding builder.
It contains a default demo template, which you can study to understand how onboardings collect data and how you can [personalize them using variables and quizzes](onboarding-user-engagement.md). Feel free to remove any screens you don't need and [design your own onboarding experience](design-onboarding.md) there.
4. When ready, click the **Preview** button at the top right. Complete your onboarding flow yourself to ensure everything works as expected.
5. If everything works fine, click **Publish** at the top right. Please wait until it is published before getting back to Adapty. Otherwise, your progress will be lost.
:::danger
If you don't click **Publish**, the SDK won't be able to get the onboarding you've created.
:::
After your onboarding is published, click **Back to Adapty**. Your onboarding is created, and you can add it to a placement to start using it.
## Step 2. Create a placement for your onboarding
1. Go to **Placements** from the main menu and switch to the **Onboardings** tab. Click **Create placement**.
2. Enter the placement name and ID. Then, click **Run onboarding** and select an onboarding to show to all users.
3. If you have a separate onboarding prepared for a specific user group, [add more audiences](audience) and select a different onboarding for them.
## Step 3. Integrate the onboarding into your app
:::important
Onboardings are available for apps using Adapty SDK v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), or v3.15.0+ (Kotlin Multiplatform, Capacitor).
:::
To start displaying onboardings in your app, integrate them using Adapty SDK:
- [iOS](ios-onboardings.md)
- [Android](android-onboardings.md)
- [React Native](react-native-onboardings.md)
- [Flutter](flutter-onboardings.md)
- [Unity](unity-onboardings.md)
- [Kotlin Multiplatform](kmp-onboardings.md)
- [Capacitor](capacitor-onboardings.md)
To understand which onboarding works better, you can also run [A/B tests](ab-tests.md).
---
# File: onboarding-layout
---
---
title: "Onboarding layout"
description: "Adapty onboarding builder: containers for layout, tweak element spacing and style."
---
The no-code mobile app onboarding builder offers two layout layers:
- Screen layout: global padding and grid via containers.
- Element layout: per-element spacing, position, borders, and shadows.
:::tip
To reorder screens or elements, simply drag and drop them in the left pane.
:::
## Screen layout
You can adjust a screen in two ways:
- [Using screen style settings](#screen-style-settings)
- [Using containers](#containers)
### Screen style settings
To reduce or increase the distance between elements and the screen edge:
1. Select the screen on the left.
2. Go to the **Styles** tab on the right.
3. Set the top, bottom, and horizontal padding in the **Padding** section.
### Containers
You may want to add side-by-side text and images, swipeable galleries, or modal pop-ups. Containers make this easy by letting you create columns, rows, carousels, and centered overlays.
To add a container:
1. Click **Add** at the top left.
2. Go to **Containers** and choose one:
- **Columns**: Split the screen into vertical sections for side‑by‑side content (e.g., two-column text or image-plus-copy layouts).
- **Rows**: Line up items in a single horizontal band with even spacing.
- **Carousel**: Let users swipe through a series of cards.
- **Popup**: Show content in a centered overlay above the page.
3. Create the elements you want to add, then drag&drop them into the container from the left menu.
## Element layout
To adjust each element individually:
1. Select the element on the left.
2. Go to **Styles** on the right menu.
3. In the **Container** section set:
- **Offset**: Shifts the element horizontally or vertically.
- **Position**: Sets the element's anchor point:
- **In content**: Normal document flow
- **Attached**: Fixed position - stays visible in viewport (e.g., sticky button at bottom)
- **Attached on scroll**: Becomes fixed after scrolling into view (sticky behavior)
- **Padding**: Defines the inner space between the element's content and its border.
- **Background**: Applies a solid color behind the element. Ensure your element background matches the [screen background](#screen-background-customization) (e.g., use grey or black for onboardings with mostly dark screens).
- **Roundness**: Determines the radius of the element's corners.
- **Border**: Adds a stroke around the element and specifies its thickness.
- **Border Color**: Specifies the color of the element's border.
- **Add shadows**: Adds a single drop shadow with configurable offset, blur/spread, and color.
:::note
In addition to these basic element layout settings, you can further customize the appearance of specific elements like [media](onboarding-media.md#media-customization), [text](onboarding-text.md#text--list-customization), [buttons](onboarding-buttons.md#button-customization), [quizzes](onboarding-quizzes.md#quiz-customization) and others using the **Styles** tab for the element.
:::
## Screen background customization
The background affects not only your onboarding design but also the loading screen until the onboarding is fully loaded.
You can fill your onboarding background with a color or upload an image/video:
1. Select the screen on the left.
2. Go to the **Styles** tab on the right.
3. In the **Background** section, select a background color or click the upload area to upload an image/video.
For media uploads, follow the [supported formats and size](onboarding-media.md#supported-formats-and-size) requirements.
:::tip
For smooth screen transitions, choose a background color that matches your overall onboarding design (e.g., use grey or black for onboardings with mostly dark screens) or customize the [splash screen](ios-present-onboardings#add-smooth-transitions-between-the-splash-screen-and-onboarding).
:::
---
# File: onboarding-media
---
---
title: "Onboarding media"
description: "Create engaging Adapty onboarding flows with images, videos, animated charts, and custom backgrounds."
---
Rich media elements help you create engaging onboarding experiences that demonstrate your app's value and guide users toward conversion. Use images and videos to showcase features, animated charts to visualize benefits, and strategic backgrounds to reinforce your brand.
## Images and videos
Images and videos are perfect for feature previews and app tours. Showing users what they'll unlock is more effective than describing it.
To upload media:
1. Click **Add** at the top left.
2. Go to **Media & Display** and choose **Image/Video**.
3. Click the upload area on the right and select your image or video to upload.
### Supported formats and size
| Specification | Details |
|--------------------|---------------------------------|
| Extensions | PNG, JPG, JPEG, WEBP, MP4, WEBM |
| Maximum file size | 15 MB |
| Maximum resolution | 1920x1920 |
If you want to add an unsupported animated element (like Lottie), you can convert it to a video (for example, with [this tool](https://www.lottielab.com/lottie/lottie-to-video)) and embed it as a video.
## Animated charts
Charts are animations that visualize results and personalize the user experience in your onboardings. To add a chart:
1. Click **Add** at the top left.
2. Go to **Media & Display** and choose **Chart**.
3. Customize your chart on the right:
- **Type**: Choose a curve type. Note that the curve type is not directly connected to the values.
- **Left** and **Right badges**: Name the initial and final points of the chart.
- **X Labels** and **Date Range**: By default, the X-axis displays dates. You can customize the date range or specify custom values.
- **Animation Duration**: Set the animation duration to fit your design.
:::tip
Use [variables](onboarding-variables.md) for dynamic data visualization in charts.
:::
## Media customization
In addition to the basic [element layout](onboarding-layout.md#element-layout), you can further customize the appearance of images, videos, and charts:
1. Select the element on the left.
2. Go to **Styles** on the right menu.
3. Based on the element type, you can adjust the following options:
- **Image/video**: Width, height, roundness, opacity, alignment.
- **Chart**: Line color and width, badge padding, roundness, font and color, X-axis font and color.
## Delete media
You can delete the entire media element or just the file to upload a new one:
- **Delete media element**: Right-click the media element on the left and select **Delete**.
- **Delete media file**: Click the media preview on the right. The upload area for your new file will appear.
---
# File: onboarding-text
---
---
title: "Onboarding text"
description: "Add and style titles, subtitles, paragraphs, and lists in Adapty’s onboarding builder, and customize text for on-brand user experiences."
---
Text elements help you create clear, personalized conversations with your users. Add titles, paragraphs, or lists with a single click, style them to match your brand, and use [dynamic variables](onboarding-variables.md) to personalize content for each user.
## Add text
You can add various text elements to your onboarding screens. To add text elements:
1. Click **Add** at the top left.
2. Go to **Typography** and choose one:
- **Title**: hero headlines or screen titles that instantly grab attention.
- **Subtitle**: a short supporting line that expands on the title.
- **Text**: body copy for feature descriptions, disclaimers, or inspirational blurbs.
- **Rich text**: mixed formatting for FAQs, terms of service, or any copy that needs links and emphasis.
3. Click the new element to edit its content.
4. (Optional) Select any part of the text to open a tooltip for quick customization—such as bold, italic, links, text color, or resetting styles.
To edit an existing text element, simply click on it and make changes in WYSIWYG mode.
:::tip
If you need to use the same text element on multiple screens, you can copy and paste it: select the element and press Ctrl+C (or ⌘+C on Mac), navigate to another screen, select the element you want to paste after, and press Ctrl+V (or ⌘+V on Mac).
:::
## Add lists
You can add numbered and bullet lists:
1. Click **Add** at the top left.
2. Go to **Typography** and choose one:
- **Numbered list**: perfect for step‑by‑step guides.
- **Bullet list**: highlight benefits or key features without implying order.
3. Go to the **Element** tab on the right to edit list items or upload an image as an item marker.
To edit an existing list element, click on it and make changes in the **Element** tab.
## Add external links
To add an external link:
1. Click **Add** at the top left.
2. In the **Typography** section, select **Title**, **Subtitle**, **Text**, or **Rich text**.
3. Enter your text.
4. Select the text you want to turn into a link.
5. Click the **Link** icon in the quick customization menu above the text.
6. Paste the external URL.
7. Click **✓** to apply the link.
:::info
In Adapty SDK versions earlier than 3.15.1, external links in onboardings open in the device’s default browser.
Starting with Adapty SDK v3.15.1, external links open in an in-app browser by default, allowing users to stay within your app without switching to another application. If needed, you can [customize this behavior](ios-present-onboardings#customize-how-links-open-in-onboardings).
:::
## Text & list customization
In addition to the basic [element layout](onboarding-layout.md#element-layout), you can customize the appearance of text and lists:
1. Select the element on the left.
2. Go to **Styles** on the right menu.
3. Based on the element type, you can adjust the following options:
- **Text**: Paragraph color, font, alignment, and line height, links color, font, and decoration.
- **List**: Text and marker text color and font, marker image width, height, and roundness.
:::tip
To speed things up:
- After customizing a text element, you can click **Apply styles to all paragraphs** below to apply the same styles across all onboarding screens in bulk.
- To change the font for all text elements on a specific screen, select the screen, then go to **Styles > Text** on the right menu.
:::
## Fonts
In the onboarding builder, you can select from a big variety of different fonts.
:::info
Uploading custom fonts is not available yet.
:::
You can set fonts globally for the whole onboarding or for each its element separately:
- To set up the main font that will be used in the onboarding:
1. Select any screen on the left.
2. Switch to the **Styles** tab and select a **Font**.
3. All the elements on all the screens will inherit the font you've selected.
- To set up a font for one element:
1. Select an element.
2. Switch to the **Styles** tab and select a **Font**.
3. The selected font will be used for this element even if you change the main font.
:::note
You can't use SF Pro, because it's not suitable for cross-platform applications, but we recommend you to use Inter instead, since they look quite similar.
:::
---
# File: onboarding-buttons
---
---
title: "Onboarding buttons"
description: "Add buttons to navigate users between screens, close the onboarding or move to the paywall."
---
Learn how to add and configure standard, animated, glossy, and countdown buttons in Adapty's no-code onboarding builder. Guide users, drive conversions, and close your flow—all without writing a single line of code.
## Add buttons
Use a Pulse Button to draw attention and boost click-through rates. Or add a Countdown Button on trial expiration slides to create urgency and increase upgrades.
To add a button:
1. Click **Add** at the top left.
2. Select **Buttons** and choose one:
- **Button**
- **Pulse Button**
- **Glossy Button**
- **Pulse Glossy Button**
- **Countdown Button**
3. Choose the [button action](onboarding-actions.md) from the **On Press** dropdown on the right:
- **Navigate**: Moves the user to a specified onboarding screen.
- **Show/Hide element**: Shows or hides a target element.
- **Open paywall**: Opens the paywall screen for purchases. Learn how to handle opening paywall on [iOS](ios-handling-onboarding-events.md#opening-a-paywall), [Android](android-handle-onboarding-events.md#opening-a-paywall), [React Native](react-native-handling-onboarding-events.md#opening-a-paywall), and [Flutter](flutter-handling-onboarding-events.md#opening-a-paywall).
- **Scroll to**: Scrolls the page to a specific element.
- **Custom**: Runs your custom event logic. For example, ut can be used for opening a login window or requesting the app permissions. Learn how to handle custom action on [iOS](ios-handling-onboarding-events.md#custom-actions), [Android](android-handle-onboarding-events.md#custom-actions), [React Native](react-native-handling-onboarding-events.md#handle-custom-actions), and [Flutter](flutter-handling-onboarding-events.md#handle-custom-actions).
- **Close onboarding**: Closes the onboarding flow. Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding),[React Native](react-native-handling-onboarding-events.md#closing-onboarding), and [Flutter](flutter-handling-onboarding-events.md#closing-onboarding).
To edit button text, click the button preview and make your changes in WYSIWYG mode.
:::tip
[Nest a popup](onboarding-layout.md#containers) with a Pulse Glossy Button to upsell premium features mid‑flow.
:::
## Button customization
Beyond the basic [element layout](onboarding-layout.md#element-layout), you can customize button appearance:
1. Select the button element on the left.
2. Go to **Styles** in the right menu.
3. Based on the button type, you can adjust these options:
- **All buttons**: Width, padding, background, roundness, border, border color, shadows, next arrow and arrow size, right offset, text or countdown color, font, and line height.
- **Pulse Button**: Animation duration and easing, shadow color and size, button grow.
- **Glossy Button**: Glossy line color, width, angle, and animation duration.
- **Pulse Glossy Button**: Animation duration and easing, shadow color and size, button grow, glossy line color, width, angle, and animation duration.
---
# File: onboarding-html
---
---
title: "Custom HTML"
description: "Embed small, lightweight HTML snippets in Adapty's no-code onboarding builder to create interactive widgets and third-party embeds."
---
Custom HTML lets you create unique interactions, embed third-party widgets, or quickly test experimental elements without app updates.
:::note
Custom HTML elements are not preloaded or cached, so we recommend using raw HTML only for small, lightweight elements.
:::
To insert custom HTML code:
1. Click **Add** at the top left.
2. Go to **Media & Display** and choose **Raw HTML**.
3. Insert or edit your HTML code on the right.
---
# File: onboarding-navigation-branching
---
---
title: "Onboarding navigation"
description: "Configure static and dynamic navigation in Adapty’s no-code onboarding builder to guide users through flows."
---
Navigation and branching lets you guide users through every step of your onboarding: use static routes to send everyone to core screens, and dynamic navigation to adapt the flow based on user choices. All without writing a single line of code.
## Set up navigation
You can configure static and dynamic navigation, as well as onboarding closure, using [buttons](onboarding-buttons.md) and [quizzes](onboarding-quizzes.md).
:::info
For quizzes, only single-answer quizzes are suitable for navigation. Multiple-answer quizzes can be used to set [conditional element visibility](onboarding-element-visibility.md).
:::
### Static navigation
Static navigation directs users to the same target screen. To set it up:
1. Add a button or a single-answer quiz.
2. Select the button or quiz and go to the **Element** tab on the right.
3. Set up the **On Press** button section or **Behaviour** for the quiz:
- **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz.
- **Action**: Select **Navigate**.
- **Data**: Select **Static** to direct users to the same target screen.
- **Destination**: Choose the destination screen.
:::note
With static navigation, a quiz directs users to the same screen regardless of the answer they select.
:::
### Dynamic navigation
Dynamic navigation routes users based on their quiz answers:
- **Quiz answers on previous screens**: Both buttons or single-answer quizzes can trigger navigation.
- **Quiz answers on the current screen**: Only single-answer quizzes can trigger navigation.
To set it up:
1. Add a button or a single-answer quiz that will navigate users.
2. Select the button or quiz and go to the **Element** tab on the right.
3. Set up the **On Press** button section or **Behaviour** for the quiz:
- **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz.
- **Action**: Select **Navigate**.
- **Data**: Select **Dynamic** to direct users based on their previous quiz answers.
- **State**: Choose a quiz whose answers determine the user destination.
4. Select the destination screen for each quiz option.
Your button or quiz will dynamically route users to the destinations you configured.
### Onboarding closure
If your user journey calls for closing the onboarding flow, you can set it up using buttons or single-answer quizzes:
1. Add a button or a single-answer quiz.
2. Select the button or quiz and go to the **Element** tab on the right.
3. Set up the **On Press** button section or **Behaviour** for the quiz:
- **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz.
- **Action**: Select **Close onboarding**.
Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding), [React Native](react-native-handling-onboarding-events.md#closing-onboarding), and [Flutter](flutter-handling-onboarding-events.md#closing-onboarding).
---
# File: onboarding-quizzes
---
---
title: "Onboarding quizzes"
description: "Add interactive quizzes to your Adapty onboardings to collect user preferences and drive personalized flows—no code needed."
---
Turn your onboarding into a two‑way conversation: add quizzes in Adapty's no‑code builder to collect preferences, segment users, and [branch flows](onboarding-user-engagement.md#onboarding-flow-branching) based on real‑time answers. You'll be gathering insights in minutes—no code required.
## Add quizzes
You can add various quiz types—text, emoji, or image options—to gather user input:
1. Click **Add** at the top left.
2. Select **Quiz** and choose one:
- **Simple**: A single-select list of text options. Use to segment users by a primary attribute (e.g., “What’s your role?”).
- **Multiple choice**: Allows selecting more than one text option. Ideal for gathering all user interests (e.g., favorite features).
- **Emoji**: Options represented by emojis for quick reactions. Great for fast sentiment checks (e.g., “How excited are you?”).
- **Media picker**: Apload images or videos as selectable choices. Perfect for choices that rely on visuals (e.g., select your favorite theme).
- **Rating**: Users rate on a numerical or star scale. Use to measure satisfaction or confidence (e.g., rate this feature 1–5).
- **Popup question**: Displays a modal question overlay. Excellent for time-sensitive prompts.
3. Set up the quiz on the right:
- **Required**: Make an answer mandatory before users can proceed.
- **Layout**: Choose between list or image tile layouts.
- **Multiple answers**: Allow multi-select (disables navigation options for the quiz).
- **Show checkboxes**: Display checkboxes when multiple answers are enabled.
4. Set up quiz options on the right:
- **Label**: Text displayed for each choice.
- **Value**: The value sent to analytics and webhook payloads.
- **Image type**: Upload media or use emojis.
5. Configure [actions](onboarding-actions.md) to fire when the user selects an option.
Learn more in the [guide on designing quizzes](#how-to-design-quizzes) below.
### How to design quizzes
Here's a simple quiz setup example.
Let's say you have a recipe app and want to know if your users are vegan or vegetarian, then learn more about their preferences based on their answer.
#### Step 1. Add screens
1. Add a new screen and add a **Quiz** element to it.
2. Add screens for different user groups. In our example, these will collect additional information, so they'll also contain quizzes.
3. Add a final screen to indicate the onboarding is complete, allowing users to go straight to the app.
#### Step 2. Configure navigation
1. To set up dynamic navigation, select the **Options** element on the first quiz screen. In the **Behavior** section, add **Action on Option**.
Since we want to redirect users to different screens based on their answers, select **Navigate** as the action, choose **Dynamic** for **Data**, and select your **Options** element in **State**. Then associate each option with a screen.
2. On both conditional screens, configure the navigation button. Since we need to skip the second conditional screen, point the navigation button directly to the screen you want to show next.
:::tip
If you want to customize the onboarding itself based on the quiz answers, see the guide for [navigation](onboarding-navigation-branching#dynamic-navigation) or for [using variables](onboarding-variables.md).
:::
## Quiz customization
Beyond the basic [element layout](onboarding-layout.md#element-layout), you can customize quiz appearance:
1. Select the quiz element on the left.
2. Go to **Styles** in the right menu.
3. Adjust these settings:
- **Options**: Height, padding, background, roundness, border, border color.
- **Text**: Color, font, alignment.
- **Pressed State**: Background, text color, border color.
:::tip
After customizing a quiz element, you can click **Apply styles to all options** below to apply the same styles across all onboarding screens in bulk.
:::
## Save quiz answers
You can also process the quiz answers in your app and store them or use them in your application.
To do this, you must handle the quiz response event in the app code. See the guide for your platform:
- [iOS](ios-handling-onboarding-events#updating-field-state)
- [Android](android-handle-onboarding-events#updating-field-state)
- [React Native](react-native-handling-onboarding-events#updating-field-state)
- [Flutter](flutter-handling-onboarding-events#updating-field-state)
---
# File: onboarding-actions
---
---
title: "Onboarding actions"
description: "Configure actions—navigate, open paywalls, fire events, and close flows—in Adapty’s no-code onboarding builder."
---
Actions are the interactive behaviors you assign to onboarding elements, allowing them to respond to user input or handle events. By setting a trigger (like a button press or loader completion) and selecting an action type, you control how users move through and interact with your onboarding flow.
:::tip
Learn more about branching onboarding flows in the detailed article.
:::
## Add actions
The configuration process depends on the element you attach the action to. You can add actions to the following elements:
- **Buttons**: Configure actions in the [**On Press** dropdown of the **Element** tab](onboarding-buttons.md#add-buttons).
- **Quizzes**: Configure actions in the [**Behaviour** section of the **Element** tab](onboarding-quizzes.md#step-2-configure-navigation).
- **Loaders**: Configure actions in the **Complete action** section of the **Element** tab.
For example, here's where to find it for quizzes:
## Action types
When configuring actions, choose one of the following types:
#### Navigate
Moves the user to another onboarding screen, letting you control flow based on user actions or selections. Ideal for chaining multiple actions for multi-step logic with quizzes.
#### Show/Hide element
Toggles the visibility of a specified element for conditional content within a screen. Use this to display extra content only when users need it.
#### Open paywall
Launches your app’s paywall to present purchases or subscriptions. Learn how to handle opening paywall on [iOS](ios-handling-onboarding-events.md#opening-a-paywall), [Android](android-handle-onboarding-events.md#opening-a-paywall), [React Native](react-native-handling-onboarding-events.md#opening-a-paywall), and [Flutter](flutter-handling-onboarding-events.md#opening-a-paywall).
#### Scroll to
Programmatically scrolls the view to a target element on the current screen. Helpful for long-form screens when a “See details” button is pressed.
#### Custom
Allows you to define and execute your own logic based on the [action ID](#action-id). Use this action to trigger behaviors not covered by the standard action types.
Learn how to handle custom action on [iOS](ios-handling-onboarding-events.md#custom-actions), [Android](android-handle-onboarding-events.md#custom-actions), [React Native](react-native-handling-onboarding-events.md#handle-custom-actions), and [Flutter](flutter-handling-onboarding-events.md#handle-custom-actions).
#### Close onboarding
Ends the onboarding flow and closes the interface. Use when users finish setup to immediately drop back into the main app.
Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding), [React Native](react-native-handling-onboarding-events.md#closing-onboarding), and [Flutter](flutter-handling-onboarding-events.md#closing-onboarding).
## Action triggers
Actions fire depending on the element they’re attached to:
- **Button**: Runs when a user clicks a button or when a timer completes.
- **Quiz**: Executes when an option is selected.
- **Loader**: Triggers after a Loader or Processing finishes.
## Action ID
:::important
Action ID is not the same as the [element ID](onboarding-variables.md) used for inserting dynamic data with variables. Be sure not to mix them up.
:::
When setting up custom actions for buttons, you may want to handle different buttons the same way using action IDs:
1. When [adding a button](onboarding-buttons.md#add-buttons), assign it an ID in the **On Press** section of the **Element** tab.
2. [Use the assigned action ID in your source code](ios-handling-onboarding-events.md#custom-actions).
::::note
On iOS devices, onboardings support only actions in the **On Press** section. The **On Press Extra** section won't work because only one view can be displayed at a time—if one action opens a view (like a paywall), the other action cannot execute simultaneously.
:::
---
# File: onboarding-variables
---
---
title: "Onboarding variables"
description: "Use dynamic variables in Adapty’s no-code onboarding builder to personalize content, capture user data, and drive tailored user flows."
---
Variables are values set based on user input or environmental data. They're essential for creating personalized and engaging onboarding experiences.
## What variables are for
Variables let you insert dynamic data—like quiz responses or user text inputs—directly into your onboarding screens. Each user sees personalized content without any coding required. For example, greet users by name using text inputs, or route quiz responders to custom follow-up screens.
You use variables by placing the element ID of the data source between double braces, like this: `{{element_id}}`.
As variables, you can use the data collected on previous screens:
- **Inputs**: The variable contains data entered by the user.
- [**Quizzes**](onboarding-quizzes.md): The variable contains the label data of selected options. If multiple answers are allowed, the variable will contain all selected options, separated by a comma and space.
:::note
Element ID is not the same as the [action ID](onboarding-actions.md#action-id) used for custom actions logic. Be sure not to mix them up.
:::
## Use variables
Here's how to use variables:
1. Create an Input element or quiz option and set its ID.
2. Use the element ID in onboarding texts in the `{{element-id}}` format. For example, you can personalize your text using the user's name.
3. When users enter their data during onboarding, it will appear dynamically wherever you've placed variables.
---
# File: onboarding-element-visibility
---
---
title: "Onboarding element visibility"
description: "Configure static and dynamic navigation in Adapty’s no-code onboarding builder to guide users through flows."
---
You can also add conditional visibility to specific elements. Conditional elements are only visible to users who gave specific quiz answers.
For example, users who answered "What's your experience level" with "Beginner" will see extra details on the next step.
To make an element conditional:
1. Go to the **Element** tab on the right.
2. Select **Conditional** in the **Visible** section.
3. Set up the condition by choosing:
- quiz ID
- operator
- quiz answer
4. (Optional) Click **Advanced condition** to add multiple conditions.
For example, if you selected 'goal' as quiz ID, 'Has' as operator, and 'Education' as quiz answer, the element will be visible only to users whose answers for that quiz include the 'Education' option.
---
# File: onboarding-metrics
---
---
title: "Onboarding metrics"
description: "Track and analyze onboarding performance metrics to improve subscription revenue."
---
Adapty collects a series of metrics to help you better measure the performance of the onboardings. All metrics are updated in real-time, except for the views, which are updated once every several minutes. This document outlines the metrics available, their definitions, and how they are calculated.
:::important
The onboarding revenue is calculated from all the transactions that occurred after the onboarding has been shown.
:::
Onboarding metrics are available on the onboarding list, providing you with an overview of the performance of all your onboardings. This consolidated view presents aggregated metrics for each onboarding, allowing you to assess their effectiveness and identify areas for improvement.
For a more granular analysis of each onboarding, you can navigate to the onboarding detail metrics. In this section, you will find comprehensive metrics specific to the selected onboarding, offering deeper insights into its performance.
## Metrics controls
The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with three indentation levels.
For Live onboardings, the metrics cover the period from the onboarding's start date until the current date. For inactive onboardings, the metrics encompass the entire period from the start date to the end of the selected time period. Draft and archived onboardings are included in the metrics table, but if no data is available, they will be listed without any displayed metrics.
### View options for metrics data
The onboarding page offers two viewing options for metrics data:
- Placement-based view: Metrics are grouped by placements associated with the onboarding. This allows users to analyze metrics by different placements.
- Audience-based view: Metrics are grouped by the target audience of the onboarding. Users can assess metrics specific to different audience segments.
The dropdown at the top of the onboarding page allows you to select the preferred view.
### Filter metrics by install date
The **Filter metrics by install date** checkbox lets you analyze data based on when users installed your app. This helps you measure how well you're acquiring new users during specific time periods. It's a handy option when you want to customize your analysis.
### Time ranges
You can analyze metrics data using a time range, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges.
### Filters and groups
Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. Adapty's metrics page gives you access to various time ranges, grouping options, and filtering possibilities.
- Filter by: Attribution, Country, Onboarding audience, Onboarding A/B tests, Onboarding placement, Paywall, Store, State.
- Group by: Product or Store.
### Single metric chart
The chart section shows your data in a simple bar graph.
The chart helps you quickly see:
- The exact numbers for each metric.
- Period-specific data.
A total sum appears next to the chart, giving you the complete picture at a glance.
Click the arrow icon to expand the chart.
### Total metrics summary
Next to the single metrics chart, there is a total metrics summary section. This section shows the cumulative values for the selected metrics at a specific point in time. You can change the displayed metric using a dropdown menu.
## Metrics definitions
### Views & unique views
Views count the number of times users see your onboarding. If someone visits twice, that's two **views** but one **unique view**. This metric helps you understand how often your onboarding has been shown.
### Completions & unique completions
Completions count the number of times users complete your onboarding, meaning that they go from the first to the last screen. If someone completes it twice, that's two **completions** but one **unique completion**.
### Unique completions rate
The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding and make changes if you notice that people ignore it.
### Revenue
**Revenue** shows your total earnings in USD from purchases and renewals. This is the amount before any deductions.
### Proceeds
[**Proceeds**](analytics-cohorts#revenue-vs-proceeds) shows what you receive after App Store/Play Store takes their commission, but before taxes.
:::important
Notify Adapty if your app is enrolled in a reduced commission program. To ensure correct calculations, specify your [Small Business Program](app-store-small-business-program) and [Reduced Service Fee program](google-reduced-service-fee) status in your [app settings](general).
:::
### Net proceeds
Your final earnings after both store commissions and taxes are deducted.
### ARPPU
ARPPU is the average revenue per paying user. It’s calculated as total revenue divided by the number of unique paying users. $15000 revenue / 1000 paying users = $15 ARPPU.
### ARPU
ARPU is the average revenue per user who viewed the onboarding. It's calculated as total revenue divided by the number of unique viewers.
### ARPAS
ARPAS shows how much money each active subscriber generates on average. Simply divide your total revenue by your number of active subscribers. For example: $5,000 revenue ÷ 1,000 subscribers = $5 ARPAS.
### CR purchases & unique CR purchases
**Conversion rate to purchases** shows what percentage of onboarding views lead to purchases. For example, 10 purchases from 100 views is 10% conversion rate.
**Unique CR purchases** measures what percentage of unique users who view your onboarding end up making a purchase, counting each user only once, regardless of how many times they see it.
### CR trials & unique CR trials
**Conversion rate to trials** shows what percentage of onboarding views lead to starting a trial. For example, 10 trials from 100 views is 10% conversion rate.
**Unique CR trials** measures what percentage of unique users who view your onboarding start a trial, counting each user only once, regardless of how many times they see it.
### Purchases
**Purchases** counts all transactions on your onboarding, except renewals. This includes:
- New direct purchases
- Trial conversions
- Plan changes (upgrades, downgrades, cross-grades)
- Subscription restores
This metric gives you a complete picture of new transaction-related activity from your onboarding.
### Trials
**Trials** counts the number of users who started free trial periods through your onboarding. This helps you track how well your trial offers attract users before they decide to pay.
### Trials cancelled
**Trials cancelled** shows how many users turned off auto-renewal during their trial period. This tells you how many people decided not to continue with a paid subscription after trying your service.
### Refunds
**Refunds** counts how many purchases and subscriptions were returned for a refund, regardless of the reason.
### Refund rate
**Refund rate** shows the percentage of first-time purchases refunded. Example: 5 refunds from 1,000 purchases = 0.5% refund rate. Renewals aren't counted in this calculation.
---
# File: get-paid-in-onboardings
---
---
title: "Connect paywalls to onboardings"
---
You can set up a seamless transition from onboardings to paywalls, so that onboardings not only improve the user experience and retention but also generate revenue for you.
There are two ways to connect paywalls to onboardings:
- **Showing a paywall after onboarding**: Implement opening a paywall after onboarding is closed.
- **Showing a paywall inside onboarding**: Trigger opening a paywall on tapping a button.
Before you start, create a [paywall](paywalls.md) and [onboarding](onboardings.md) and add them to placements.
:::important
You need two different placements: one for a paywall and another for an onboarding. Ensure you use proper placement IDs when getting onboarding and paywall in your code.
:::
## Show paywall after onboarding
To show a paywall after onboarding, you only need to handle an event generated each time users close the onboarding.
As soon as users close onboarding, the [event](ios-handling-onboarding-events#closing-onboarding) is triggered. So, if you want to display a paywall after your onboarding immediately, you can implement
3. Now, when you have this button, each time your users tap it, it will generate an action containing the action ID.
To handle this action in your app code, you will need to [get the paywall](fetch-paywalls-and-products.md) and then [display it](ios-quickstart-paywalls.md).
---
# File: target-onboardings-to-different-user-groups
---
---
title: "Target onboardings to different user groups"
description: "Display different onboardings based on the acquisition source or other user attributes."
---
You can display different onboarding flows based on user attributes: acquisition source, campaign, geography, device type, lifecycle state, or user intent. Matching onboarding content to specific user groups improves activation rates and early engagement. The setup involves both developer work and dashboard configuration.
## Before you start
- **SDK version**: Your app must use Adapty SDK v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), or v3.15.0+ (Kotlin Multiplatform, Capacitor). See [Onboardings](onboardings) for details.
- **Onboardings**: Create at least two onboardings in Adapty before starting — one default and one per segment. See [Create onboarding](create-onboarding).
- **Attribution tools**: If your app uses AppsFlyer, Adjust, Branch, or another [attribution integration](attribution-integration), campaign and source data may already be available as user attributes in Adapty. You can use this data directly in segments — skip to Step 2.
## Step 1. Assign custom attributes to users *(Developer)*
Call `updateProfile` early in the user's session, before the onboarding is displayed. The attribute must be available when Adapty evaluates which onboarding to show.
2. Choose **Language**.
3. **Optional**: Enable **Translate with AI** to automatically translate all content from the original onboarding into the selected language.
4. Click **Create**.
Now, you can translate the content manually, use AI, or export the localization file for external translators. Adapty will create a copy of your onboarding where you can modify both the text content and visual elements to suit your target audience.
:::tip
You can customize the design of each locale (images, colors, layouts) to better target different audiences. However, structural changes (adding or removing screens and elements) can only be made in the default locale.
:::
## Translate onboardings using AI
AI-powered translation provides a quick and efficient way to localize your onboarding. Translation typically takes 1-2 minutes depending on the onboarding size.
You can use AI translation at two different stages:
- **When adding a new locale:** Check **Translate with AI** during locale creation to generate a pre-translated version.
- **After creating a locale:** Navigate to the globe icon → **Manage locales**, then click **Translate with AI** next to your desired locale.
:::important
Any manual edits you've made to the localized version will be overwritten. Additionally, changes made to the default locale after AI translation won't be automatically reflected in translated versions – you'll need to re-run the translation.
:::
## Export localization files for external translation
You can export localization files to share with your translators and then import the translated results back into Adapty.
Exporting by the **Export** button creates one `.tsv` file for all languages.
## Import files
Once you’ve received the translated file, use the **Import** button to upload it. Adapty will automatically validate the files to ensure they match the correct format and paywall configuration structure.
:::tip
If you send this file to several translators at the same time, remember to remove extra columns from the file when importing. Otherwise, some translations will be overwritten by unchanged columns.
:::
### Import file format
To ensure a successful import, the import file must meet the following requirements:
- **File extension:**
The file must have a `.tsv` extension.
- **Only tabulations as separators**:
Use tabulations as separators. Other separators will result in errors.
- **Header line**:
The file must include a header line.
- **Correct column names:**
The column names must be **Key** and full localization names.
- **Correct Key names**: Values in the **Key** column must remain unchanged, as they correspond to screen and element identifiers..
- **No additional entities:**
Ensure the file doesn’t include entities not present in the current onboarding configuration. Extra entities will result in errors.
- **Partial import:**
The file can include all or just some entities from the current paywall configuration.
## Existing limitations
Once a user opens an onboarding, the displayed language is locked for approximately 24 hours. If the user changes their device or app language during this time, the onboarding will continue to display in the original language. After 24 hours, the new language will be applied.
This affects two scenarios:
- User opens onboarding in language A, closes the app, changes device language to language B, and reopens the app → onboarding still displays in language A
- User opens onboarding in language A, closes it, changes in-app language to language B, and reopens the onboarding → onboarding still displays in language A
---
# File: onboarding-version-control
---
---
title: "Onboarding version control"
description: "Restore previous versions of your onboardings."
---
Whenever you **Save**, **Preview**, or **Publish** your onboarding, Adapty adds the current version to your **version history**. You can browse the version history and switch to any of the saved versions.
:::warning
Adapty doesn't autosave your onboardings during the [editing process](design-onboarding). **Publish** your onboarding if you want to add it to your version history.
:::
Open the "versions" menu in the top right corner of the page to view your version history. Each version contains a timestamp to let you know when it was saved. Click the arrows button on one of the entries to revert to this state.
:::important
Remember to **publish** the changes after you revert to an earlier version.
:::
---
# File: onboarding-offline
---
---
title: "Offline mode"
description: "Handle trying to open an offboarding offline."
---
An internet connection is required to fetch the onboarding flow from the server. This is essential for retrieving the onboarding data and displaying the onboarding sequence and the paywall with products that follow. Both onboarding and paywall content are dynamically loaded from the server, ensuring they are always up-to-date.
## Offline mode
To optimize the user experience, the onboarding sequence is loaded this way:
- **Initial screen load**: Only the first screen of the onboarding flow is required to be loaded initially. This allows us to minimize load times, even on slower mobile connections such as 3G or 4G.
- **Preloading**: Once the first screen is loaded and displayed, we immediately start preloading the subsequent screens (including fonts, videos, images) in the background.
If a user loses internet connectivity during the onboarding process, they will encounter an error screen with two options:
- **Try again**: Upon tapping **Try again**, the system will reattempt to load the onboarding flow. If the connection is restored and the content is successfully loaded, the onboarding will resume from where the user left off, with all progress preserved.
- **Close**: If the user decides to close the onboarding, the [close](ios-handling-onboarding-events#closing-onboarding) event with `"action_id": "error"` will be triggered.
---
# File: local-fallback-onboarding
---
---
title: "Download fallback onboardings"
description: "Use local fallback onboardings in Adapty to ensure seamless subscription flows."
---
To load an [onboarding](onboardings.md), your application requests its configuration data from Adapty. Onboarding configs store the URLs of your onboarding flows. If a network issue disrupts the connection between your application and the Adapty servers, you cannot correctly configure and display your onboardings.
To access onboarding configuration data offline, store a fallback configuration file inside your app code. You can download ready-made fallback files with configuration data for your onboardings and [paywalls](fallback-paywalls) directly from Adapty.
Follow the instructions below to download the file and add it to your application code.
:::important
Fallback onboardings **require an internet connection**, since onboarding content is always stored online. The fallback file only stores the onboardings' configuration.
Read the [onboarding offline mode](onboarding-offline) article to understand what happens when the application cannot load the onboarding.
:::
2. Switch to the **Remote config** tab.
Remote config has two views:
- [Table](customize-paywall-with-remote-config#table-view-of-the-remote-config)
- [JSON](customize-paywall-with-remote-config#json-view-of-the-remote-config)
Both the **Table** and **JSON** views include the same configuration elements. The only distinction is a matter of preference, with the sole difference being that the table view offers a context menu, which can be helpful for correcting localization errors.
You can switch between views by clicking on the **Table** or **JSON** tab whenever necessary.
Whatever view you've chosen to customize your onboarding, you can later access this data from SDK using the `remoteConfig` property of `AdaptyOnboarding`, and make some adjustments to your onboarding.
You can combine different options and create your own.
### JSON view of the remote config
In the **JSON** view of the remote config, you can enter any JSON-formatted data:
### Table view of the remote config
If you don't often work with code and need to correct some JSON values, Adapty has the **Table** view for you.
It is a copy of your JSON in a table format that is easy to read and understand. Color coding helps to recognize different data types.
To add a key, click **Add row**. We automatically check the values and types mapping and show an alert if your corrections may lead to an invalid JSON.
---
# File: profiles-crm
---
---
title: "Profiles/CRM"
description: "Manage user profiles and CRM data in Adapty to enhance audience segmentation."
---
Profiles is a CRM for your users. With Profiles, you can:
1. Find specific users by profile ID, customer user ID, email, or transaction ID.
2. Explore the full payment path of a user including billing issues, grace periods, and other [events](events).
3. Analyze user's properties such as subscription state, total revenue/proceeds, and more.
4. Grant the user a subscription.
## Subscription state
In a full table of subscribers, you can filter, sort, and find users. The state describes the user state in terms of a subscription and can be:
| User **state** | Description |
| :--------------------- | :----------------------------------------------------------- |
| Subscribed | The user has an active subscription with auto-renewal enabled. |
| Auto-renew off | The user turned off auto-renewal but still has access to premium features until the end of the subscription period. |
| Subscription cancelled | The user canceled their subscription, and it has fully ended. |
| Billing issue | The user couldn’t be charged due to a billing issue, either after their subscription or trial expired. |
| Grace period | The user is currently in a grace period due to a billing issue that occurred when attempting to charge them after their subscription or trial expired. |
| Active trial | The user has an active subscription that is currently in its trial period. |
| Trial cancelled | The user canceled the trial and does not have an active subscription. |
| Never subscribed | The user has never subscribed or started a trial and remains a freemium user. |
## User attributes
You can send any properties that you want for the user.
By default, Adapty sets:
| Property | Description |
| ---------------- | ------------------------------------------------------------ |
| Customer user ID | An identifier of your end user in your system. |
| Adapty ID | Internal Adapty identifier of your end user, called Profile ID. |
| IDFA | The Identifier for Advertisers, assigned by Apple to a user's device. |
| Country | Country of your end user. |
| OS | The operating system used by the end user. |
| Device | The end-user-visible device model name. |
| Install date | The date when the user was first recorded in Adapty:
## Grant a subscription
In a profile, you can find an active subscription. At any time you can prolong the user's subscription or grant lifetime access.
It's most useful for users without an active subscription so you can grant the individual user or a group of users premium features for some time. Please note that adjusting the subscription date for active subscriptions will not impact the ongoing payments.
:::note
**Expires at** must be a date in the future and can't be decreased ones set.
:::
## Profile record creation
Adapty creates an internal profile ID for every user to track their purchases, events, and subscription state. You can optionally [set a Customer User ID](identifying-users) to identify users in your own system.
**Without a Customer User ID**, a new profile is created each time:
- A user launches your app for the first time after installation
- A user reinstalls the app
- A user logs out of your app
**With a Customer User ID**, profile behavior depends on when you identify:
- **Identify during SDK activation**: Adapty uses the existing profile with that customer user ID (for returning users) or creates a new profile (for first-time users).
- **Identify after SDK activation**: Adapty creates an anonymous profile on activation. When you later identify the user, it links that customer user ID to the anonymous profile (for first-time users) or switches to the existing profile with that ID (for returning users).
Using a customer user ID gives you several advantages:
1. You can track a user across app reinstalls and multiple devices, making it easier to maintain a complete user history.
2. You can find users by their customer user ID in the [**Profiles**](profiles-crm) section and view their transactions and events.
3. You can use the customer user ID in the [server-side API](getting-started-with-server-side-api).
4. The customer user ID will be sent to all integrations.
### Parent and inheritor profiles
When multiple profiles are connected to the same subscription (through the same Apple/Google ID), Adapty tracks them using a parent-inheritor relationship. This happens when a user reinstalls the app without identifying, or when different identified users restore purchases on the same device.
The **parent profile** is the one that made the purchase—not necessarily the first profile created. For example, if you install the app, don't buy anything, then reinstall and purchase a subscription, your second profile becomes the parent (it made the purchase), while your first profile becomes the inheritor (it gains access through sharing).
**How events are distributed:**
- **Transactional events** (purchases, renewals, cancellations, billing issues, grace periods, refunds): Only appear on the **parent profile** that made the purchase. All subscription renewals and updates continue appearing on this profile.
- **access_level_updated events**: Appear on **both parent and inheritor profiles** whenever the access level state changes, ensuring all connected profiles stay updated about their current access status.
This means the parent profile (the one that purchased) shows the complete transaction history, while inheritor profiles show only their access level updates along with a link to the parent profile in the **Access level** section.
The [**Sharing paid access between user accounts**](#sharing-paid-access-between-user-accounts) setting controls which profiles receive access level updates:
Enabled (default): Both parent and inheritor profiles receive access_level_updated events as long as access remains shared
Transfer access to new user: When access is transferred, the new owner becomes the active profile and receives access_level_updated events, while the previous owner stops receiving them
Disabled: Only the parent profile maintains the access level. Inheritor profiles don't receive access unless explicitly granted
## Event timestamps with future dates
Why do events show future timestamps in profiles and integrations? Event timestamps may appear with future dates in profiles and integrations because Apple sends renewal events in advance.
- **Why it happens**: Apple does this to ensure subscriptions renew automatically before expiring, preventing user service interruptions. For more details, check Apple’s Developer Forum: [Server Notifications for Subscriptions](https://developer.apple.com/forums/tags/app-store-server-notifications).
- **Event types affected**: Typically, this applies to subscription renewals and trial-to-paid conversions. These events may have future timestamps because Apple notifies systems about them ahead of time.
All other events—like additional in-app purchases or subscription plan changes—are recorded with their actual timestamps since they cannot be predicted in advance.
- **Impact on Analytics and Event Feed**: These events will only appear in **Analytics** and the **Event Feed** once their timestamps have passed. Events with future timestamps are not shown in either section.
- **Impact on Integrations**: Adapty sends events to integrations as soon as they are received. If an event has a future timestamp, it will be shared with your integration exactly as received.
## Sharing paid access between user accounts
:::link
Main article: [Sharing paid access between user accounts](sharing-paid-access-between-user-accounts)
:::
Access the [General](general) settings page to set your access level sharing policy. You can select a separate policy for the [sandbox environment](test-purchases-in-sandbox).
---
no_index: true
---
**Enabled (default)**
Identified users (those with a [Customer User ID](identifying-users#setting-customer-user-id-on-configuration)) can share the same [access level](https://adapty.io/docs/access-level) provided by Adapty if their device is signed in to the same Apple/Google ID. This is useful when a user reinstalls the app and logs in with a different email — they’ll still have access to their previous purchase. With this option, multiple identified users can share the same access level.
Even though the access level is shared, all past and future transactions are logged as events in the original Customer User ID to maintain consistent analytics and keep a complete transaction history — including trial periods, subscription purchases, renewals, and more, linked to the same profile.
**Transfer access to new user**
Identified users can keep accessing the [access level](access-level) provided by Adapty, even if they log in with a different [Customer User ID](identifying-users#setting-customer-user-id-on-configuration) or reinstall the app, as long as the device is signed in to the same Apple/Google ID.
Unlike the previous option, Adapty transfers the purchase between identified users. This ensures that the purchased content is available, but only one user can have access at a time. For example, if UserA buys a subscription and UserB logs in on the same device and restores transactions, UserB will gain access to the subscription, and it will be revoked from UserA.
If one of the users (either the new or the old one) is not identified, the access level will still be shared between those profiles in Adapty.
Although the access level is transferred, all past and future transactions are logged as events in the original Customer User ID to maintain consistent analytics and keep a complete transaction history — including trial periods, subscription purchases, renewals, and more, linked to the same profile.
After switching to **Transfer access to new user**, access levels won’t be transferred between profiles immediately. The transfer process for each specific access level is triggered only when Adapty receives an event from the store, such as subscription renewal, restore, or when validating a transaction.
**Disabled**
The first identified user profile to get an access level will retain it forever. This is the best option if your business logic requires that purchases be tied to a single Customer User ID.
Note that access levels are still shared between anonymous users.
You can "untie" a purchase by [deleting the owner’s user profile](ss-delete-profile). After deletion, the access level becomes available to the first user profile that claims it, whether anonymous or identified.
Disabling sharing only affects new users. Subscriptions already shared between users will continue to be shared even after this option is disabled.
:::warning
Apple and Google require in-app purchases to be shared or transferred between users because they rely on the Apple/Google ID to associate the purchase with. Without sharing, restoring purchases might not work upon subsequent reinstalls.
Disabling sharing may prevent users from regaining access after logging in.
We recommend disabling sharing only if your users **are required to log in** before they make a purchase. Otherwise, an identified user could buy a subscription, log into another account, and lose access permanently.
:::
### Which setting should I choose?
| My app... | Option to choose |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| Does not have a login system and only uses Adapty’s anonymous profile IDs. | Use the default option, as access levels are always shared between anonymous profile IDs for all three options. |
| Has an optional login system and allows customers to make purchases before creating an account. | Choose **Transfer access to new user** to ensure that customers who purchase without an account can still restore their transactions later. |
| Requires customers to create an account before purchasing but allows purchases to be linked to multiple Customer User IDs. | Choose **Transfer access to new user** to ensure that only one Customer User ID has access at a time, while still allowing users to log in with a different Customer User ID without losing their paid access. |
| Requires customers to create an account before purchasing, with strict rules that tie purchases to a single Customer User ID. | Choose **Disabled** to ensure that transactions are never transferred between accounts. |
### Access sharing history
When access levels are shared or transferred, you might want to know who granted access to the current user or who the current user shared their access with. To find out, just open the user’s **Profile** and click the link to view the connected profile.
---
# File: sharing-paid-access-between-user-accounts
---
---
title: "Sharing paid access between user accounts"
description: "Sharing paid access between different user accounts to accommodate users with multiple devices or multiple app profiles"
---
When a user makes a purchase, Adapty assigns a new [access level](access-level) to their active [profile](identifying-users). This access level authorizes the buyer to access paid content.
The buyer's profile may inadvertently change if they reinstall your app, or log into a new in-app account. To ensure uninterrupted access, Adapty automatically shares the user's access level between the original profile and the ones that follow.
This approach works best for most applications. But if your business logic demands it, you can select a more limited paid access sharing policy.
Open the [General Settings](https://app.adapty.io/settings/general) page to set an access level sharing policy. To facilitate testing, you can change this setting for the [sandbox environment only](#sharing-paid-access-on-sandbox).
## Enabled (default)
This setting works best for applications **without built-in authentication**. After the purchase, all profiles associated with same store account automatically *inherit* the access level.
* If a user logs into your app with a new set of credentials, they retain access to paid content.
* If a user reinstalls your application after a factory reset, they retain access to paid content.
* If a user installs the application on other devices with the same store account, the purchase is made available on all devices. Even if each instance of the application has its own customer profile.
## Transfer access to new user
This setting works best for applications that allow purchases **with or without authentication**, or want to enforce a **one-device-per-user** policy.
Adapty limits purchase access to 1 customer ID at a time. The device owner can reinstall the app, log in and out, but cannot access the same product from more than one customer ID simultaneously.
With this setting enabled, anonymous profiles (for example, a profile that becomes active after the user logs out) always inherit the access level of the last active customer ID. This is necessary to prevent loss of access later on.
:::warning
When you disable the default setting, and enable **Transfer access to new user**, Adapty does not immediately update the access levels of existing customer profiles.
The switch occurs when a user triggers a new store event: for example, renews the subscription, or restores their purchases.
:::
## Disable paid access sharing
This setting is **only appropriate** for applications with **mandatory authentication** or an independent access management implementation. In other cases, the users may not be able to access their purchases, and your application risks **failing the mandatory store review**.
If you disable paid access sharing, Adapty ties the product to the active [customer ID](identifying-users#setting-customer-user-id-on-configuration) at the time of the purchase, and does not share the access level with any other customer profiles. This policy allows for strict 1-to-1 product distribution.
:::warning
When you disable paid access sharing, you prevent customer IDs from inheriting paid access. If a customer ID inherited paid access in the past, it cannot be revoked automatically.
:::
:::important
In emergency situations, you may need to [delete a user profile](https://adapty.io/docs/api-adapty/operations/deleteProfile) in order for the next available profile (whether identified or anonymous) to claim its access level.
:::
## Sharing paid access on sandbox
You can set a sharing paid access policy specifically for the sandbox environment. When you test purchases in the sandbox environment, expect the following behavior:
* Apple stores information about your past purchases in the account's purchase history. The Adapty SDK can access it, too.
* If you reinstall the application, and Adapty detects that the product has already been purchased, the active profile will inherit the access level.
* If Apple detects an existing purchase for the product, it won't allow you to make the same purchase twice, even if the active profile doesn't have the necessary access level.
This behavior occurs **independently of your sharing paid access setting**. Your app doesn't display the paywall, you can't buy the product. The only solution is to **clear your account's purchase history**. Follow the [sandbox testing guide](test-purchases-in-sandbox) for in-depth instructions.
## Paid access sharing in analytics
* Adapty logs transactions as they occur. A single transaction may be associated with more than one profile, but isn't counted more than once.
* If two or more profiles share the same access level, the purchase is attributed to the [parent profile](profiles-crm#parent-and-inheritor-profiles).
* Access level inheritance does not impact installation statistics. To determine how Adapty counts installations, you can select one of the two available [installation definitions](installs#calculation) on the settings page.
---
# File: segments
---
---
title: "Segments"
description: "Create and manage user segments for better targeting in Adapty."
---
A **Segment** is a set of filters that groups users with shared properties. Use segments to target paywalls and A/B tests more effectively.
After you create a segment, you can [use it as an **audience** in Placements and A/B tests](audience) to control which paywall users see (single or multiple). Examples:
- Show a standard paywall to non‑subscribers and offer a discount to users who previously canceled a subscription or trial.
- Display different paywalls to users from different countries.
- Target users based on Apple Search Ads attribution data.
- Ensure users on older app versions keep seeing the existing paywall, while newer versions get the updated one.
- [In Analytics](controls-filters-grouping-compare-proceeds.md#filtering-and-grouping), filter by segments to view performance for specific user groups. Group by segment to compare performance or contribution within **All users**.
## Creation
To create a segment, enter a name and select the attributes that define its filters. When you select multiple attributes, users must match all conditions. Adapty applies AND logic between attributes.
## Available attributes
:::note
While many user attributes are set automatically (like **Country** or **Calculated total revenue USD**), **Age**, **App user ID**, **Attribution** data, **Gender**, and **Custom attributes** are not defined automatically. You must [set user attributes](setting-user-attributes.md) or [pass the attribution data](attribution-integration.md) if you want to use it for segmentation.
:::
:::tip
For date-based attributes, you can filter using:
- **Fixed date**: Select specific dates from a calendar (e.g., show a special offer to users who installed between Black Friday and Cyber Monday)
- **Relative range**: Set dynamic time windows like "Last 7 days" or "Last 3 months" (e.g., re-engage users who were last seen 30+ days ago, or target recent installs)
Relative ranges automatically update, making them ideal for ongoing campaigns. Fixed dates work best for time-bound promotions.
:::
| Attribute | Filter by |
|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Age** | The user's age. Note that age is calculated when Adapty first receives it and is not updated later. |
| **App User ID** | The user's identifier in your app ([customer_user_id](profiles-crm#user-properties)). You can filter by its presence or absence, for example, to show a paywall only to users who haven’t logged in. |
| **App version (current)** | The current version of the app installed on the user's device where Adapty last received event data. When creating a segment, select a pencil icon next to **App version** and add a new version so you can use it right away.
| Field | Description |
| ------ |--------------------------------------------------------------------------------------------------------------------------------------|
| **Name** | A label for the custom attribute, used only in the Adapty Dashboard. |
| **Key** | A unique identifier for the attribute. This must match the key used in the SDK. |
| **Type** | Choose between:
## Duplicate segments
If you need a segment similar to an existing one, duplicate it instead of building it from scratch. This saves time for teams that run multiple campaigns or A/B tests with overlapping user groups.
Duplicating a segment creates a copy with all its filters and description. The new segment will have "(copy)" added to its name so you can tell it apart from the original. The new segment is independent of the original. Changes to one do not affect the other.
To duplicate a segment in the Adapty Dashboard:
1. Open the **Profiles & Segments** section in the Adapty main menu and switch to the [**Segments**](https://app.adapty.io/segments) tab.
2. Click the **3-dot** button next to the segment and select **Duplicate**.
3. Open the new segment and adjust its filters as needed.
---
# File: event-feed
---
---
title: "Event feed"
description: "Monitor and analyze user activity with Adapty’s event feed."
---
Event feed allows you to visually track [Events](events) generated by Adapty and check the status of their export to 3rd-party integrations, including the webhook.
:::warning
Keep in mind that transactions created using the [server-side API (version 1)](server-side-api-specs-legacy#requests) do not appear in the **Event Feed**. To ensure they are included, use the [server-side API (version 2)](api-adapty/operations/setTransaction) instead.
:::
:::note AppsFlyer, Facebook Ads, and Branch sending status could be inaccurate because they do not always return errors when they occur. ::: To view the profile of the user who has initiated the transaction, click the **View Profile** button in the event details. --- # File: ab-tests --- --- title: "A/B test" description: "Optimize subscription pricing with A/B tests in Adapty for better conversion rates." --- Boost your app revenue by running A/B tests in Adapty. Compare different paywalls and onboardings to find what converts best — no code changes needed. For example, you can test: - Subscription prices - Paywall design, copy, and layout - Trial periods and subscription durations - Onboardings This guide shows how to create A/B tests in the Adapty Dashboard and read the results. :::warning If you are not using the [Adapty paywall builder](adapty-paywall-builder), ensure you [send paywall views to Adapty](present-remote-config-paywalls#track-paywall-view-events) using the `.logShowPaywall().` Without this method, Adapty wouldn't be able to calculate views for the paywalls within the test, which would result in irrelevant conversion stats. ::: ## A/B test types Adapty offers three A/B test types: - **Regular A/B test:** An A/B test created for a single [paywall](https://adapty.io/docs/paywalls) placement. - **Onboarding A/B test:** An A/B test created for a single [onboarding](https://adapty.io/docs/onboardings) placement. - **Crossplacement A/B test:** An A/B test created for multiple paywall placements in your app. Once a
### Key differences
| Feature | Regular A/B Test | Crossplacement A/B Test |
| ------------------------------- |--------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| **What is being tested** | One paywall/onboarding | Set of paywalls belonging to one variant |
| **Variant consistency** | Variant is determined separately for every placement | Same variant used across all paywall placements |
| **Audience targeting** | Defined per paywall/onboarding placement | Shared across all paywall placements |
| **Analytics** | You analyse one paywall/onboarding placement | You analyze the whole app on those placements that are a part of the test |
| **Variant weight distribution** | Per paywall/onboarding | Per set of paywalls |
| **Users** | For all users | Only new users (those who haven’t seen an Adapty paywall) |
| **Adapty SDK version** | Any for paywalls. For onboardings: v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), v3.15.0+ (KMP, Capacitor) | 3.5.0+ |
| **Best for** | Testing independent changes in a single paywall/onboarding placement without considering the overall app economics | Evaluating overall monetization strategies app-wide |
Each paywall/onboarding gets a weight that splits traffic during the test.
For instance, with weights of 70 % and 30 %, the first paywall is shown to roughly 700 of 1,000 users, the second to about 300. In cross-placement tests, weights are set per variant, not per paywall.
This setup allows you to effectively compare different paywalls and make smarter, data-driven decisions for your app's monetization strategy.
## A/B test selection logic
As you may have noticed from the table above, **cross-placement A/B tests take priority over regular A/B tests**. However, cross-placement tests are only shown to **new users** — those who haven’t seen a single Adapty paywall yet (to be precise, `getPaywall` SDK method was called). This ensures consistency in results across placements.
Here is an example of the logic Adapty follows when deciding which A/B test to show for a paywall placement:
Regular, onboarding, and cross-placement tests appear on separate tabs that you can switch between.
## Crossplacement A/B test limitations
Currently, crossplacement A/B tests cannot include onboarding placements.
Crossplacement A/B tests guarantee the same variant across all placements in the A/B test, but this creates several limitations:
- They always have the highest priority in a placement.
- Only new users can participate, i.e. the users who have not seen a single Adapty paywall before (to be precise, `getPaywall` SDK method was called). That is done because it's not possible to guarantee for the old users that they will see the same paywall chain, because an existing user could have seen something before the test has been started.
:::important
By default, once a user is assigned to a cross-placement test variant, they stay in that variant for 3 months, even after you stop the test. To override this behavior and allow showing other paywalls and A/B tests, configure the **[Cross-placement variation stickiness](general#9-cross-placement-variation-stickiness)** setting in the **App settings**. However, note that, even then, they won't be able to be a part of any other cross-placement test ever.
:::
:::note
In Analytics, a cross-placement A/B test appears as several child tests — one per placement. They follow the naming pattern `
3. Click **Create A/B test** at the top right.
3. In the **Create the A/B test** window, enter a **Test name**. This is required and should help you easily identify the test later. Choose something descriptive and meaningful so it's clear what the test is about when you review the results.
4. Fill in the **Test goal** to keep track of what you're trying to achieve. This could be increasing subscriptions, improving engagement, reducing churn, or anything else you're focusing on. A clear goal helps you stay on track and measure success.
5. Click **Select placement** and choose a paywall placement for a regular A/B test or an onboarding placement for an onboarding A/B test.
6. Set up the test content in the **Variants** table. Each row is a variant, each column is a placement. Add a paywall at each intersection.
By default, the table has 2 variants and 1 placement. You can add up to 20 variants and multiple placements. Once you add a second placement, the test becomes a cross-placement A/B test.
Key:
| 1 | Rename the variant to make it more descriptive. |
| 2 | Change the weight of the variant. The total of all variants must equal 100%. |
| 3 | Add more variants if needed. |
| 4 | Add more placements if needed. |
| 5 | Add paywalls or onboardings to display in the placements for every variant. |
3. Switch to the **Drafts** tab. Only draft tests can be started.
4. Click the **Run A/B test** button next to the test you want to launch.
5. The **Edit A/B test** window will open so you can review and make any final changes before launching the test. If something important is missing — like the placement or audience — you’ll be able to add it now. Keep in mind that once the test is live, you won’t be able to make any edits. To apply changes later, you’ll need to stop the test and create a new one.
6. Once everything looks good, click **Run A/B test** to start.
After launching the test, you can track its progress and view performance data on the [A/B test metrics](results-and-metrics) page. This helps you spot the best-performing variation and make smarter decisions. For more details on how Adapty calculates these metrics, check out Math behind the A/B tests.
## How to stop the A/B test
Stopping an A/B test means you're ready to end it and review the results. This step is key for wrapping up the test and deciding what to show users next.
1. Open the [A/B tests](https://app.adapty.io/ab-tests) section and go to the **Live** tab.
2. Click the three-dot menu next to the test you want to stop, then choose **Stop A/B test**.
3. In the **Stop the A/B test** window, decide what should happen after the test ends. You have three options:
| Option | Description |
|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Display one of the tested paywalls/onboardings | Choose the winning paywall or onboarding based on test results like revenue, probability to be best (**P2BB**), and revenue per 1K users. This paywall or onboarding will be shown for the selected placement and audience. |
| Select paywalls/onboardings that don’t participate in A/B test | In this case, you can choose any paywall or onboarding that isn’t part of the current A/B test. This is helpful when the test results show that none of the tested items performed well, and you want to continue with a more effective one instead. |
| Don’t show any specific paywall/onboarding | For the selected placement and audience, no specific paywall or onboarding will be selected after the A/B test ends. Instead, the next available paywall or onboarding based on audience priority will be shown. This is a good choice if you’d rather let your existing setup decide which paywall or onboarding to display, without manually selecting one. |
4. Click the **Stop and complete this A/B test** button.
Once the A/B test is finished, it will no longer be active, and the paywalls or onboardings from it will no longer be displayed to new users.
You can still access the A/B test results and metrics on the [A/B test metrics page](results-and-metrics#metrics-controls) to review performance for users who participated while the test was running. Metrics may continue to update as new purchase or revenue events are attributed to those users.
:::note
Stopping an A/B test is irreversible, and the test cannot be restarted once it has been stopped. Ensure that you have gathered sufficient data and insights before making the decision to stop an A/B test.
:::
---
# File: results-and-metrics
---
---
title: "A/B test results and metrics"
description: "Analyze results and key metrics in Adapty to improve your app’s subscription performance and user engagement."
---
Discover important data and insights from our [A/B tests](ab-tests), comparing different paywalls and onboardings to see how they affect user behavior, engagement, and conversion rates. By looking at the metrics and results here, you can make smart choices and improve your app's performance. Dive into the data to find actionable insights and enhance your app's success.
### A/B test results
Here are three metrics that Adapty provides for A/B test results:
**Revenue**: This metric shows the total amount of money generated in USD from purchases and renewals, minus any refunds given to users. It includes both the initial purchase and any follow-up subscription renewals. Revenue helps you understand how each A/B test variant is performing financially and figure out which one brings in the most money.
Learn more about [paywall](https://adapty.io/docs/paywall-metrics) and [onboarding](https://adapty.io/docs/onboarding-metrics) metrics.
**Probability to be best**: Adapty utilizes a robust mathematical analysis framework to analyze A/B test results and provides a metric called Probability to be best. This metric assesses the likelihood that a particular variant is the best-performing option (in terms of its long-term revenue) among all the variants tested. The metric is expressed as a percentage value ranging from 1% to 100%. For detailed information on how Adapty calculates this metric, please refer to the [documentation.](maths-behind-it)The best performing option, determined by Revenue per 1K user, is highlighted in green and automatically selected as the default choice.
**Revenue per 1K users**: The revenue per 1K users metric calculates the average revenue generated per 1,000 users for each A/B test variant. This metric helps you understand the revenue efficiency of your variants, regardless of the total number of users. It allows you to compare the performance of different variants on a standardized scale and make informed decisions based on revenue generation efficiency.
**Prediction intervals for revenue 1K users**: The revenue per 1K users metric also includes prediction intervals. These prediction intervals represent the range within which the true revenue per 1,000 users for a given variant is predicted to fall based on the available data and statistical analysis.
In the context of A/B testing, when analyzing the revenue generated by different variants, we calculate the average revenue per 1,000 users for each variant. Since revenue can vary among users, the prediction intervals provide a clear indication of the plausible values for the revenue per 1,000 users, taking into account the variability and uncertainty associated with the prediction process.
By incorporating prediction intervals into the revenue per 1K users metric, Adapty enables you to assess the revenue efficiency of your A/B test variants while considering the range of potential revenue outcomes. This information helps you make data-driven decisions and optimize your subscription strategy effectively, by taking into account the uncertainty in the prediction process and the plausible values for revenue per 1,000 users.
By analyzing these metrics provided by Adapty, you can gain insights into the financial performance, statistical significance, and revenue efficiency of your A/B test variants, enabling you to make data-driven decisions and optimize your subscription strategy effectively.
## A/B test metrics
Adapty provides a comprehensive set of metrics to help you effectively measure the performance of your A/B test conducted on your paywall or onboarding variations. These metrics are continuously updated in real-time, except for views, which are updated periodically. Understanding these metrics will help you assess the effectiveness of different variations and make data-driven decisions to optimize your paywall or onboarding strategy.
A/B test metrics are available on the A/B test list, where you can gain an overview of the performance of all your A/B tests. This comprehensive view offers aggregated metrics for each test variation, enabling you to compare their performance and identify significant differences. For a more detailed analysis of each A/B test, you can access the A/B Test detail metrics. This section provides in-depth metrics specific to the selected A/B test, allowing you to delve into the performance of individual variations.
All metrics, except for views, are attributed to the product within the paywall or onboarding.
## Metrics controls
The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with three levels of indentation.
### Profile install date filtration
The Filter metrics by install date checkbox enables the filtering of metrics based on the profile install date, instead of the default filters that use trial/purchase date for transactions or view date for paywall or onboarding views. By selecting this checkbox, you can focus on measuring user acquisition performance for a specific period by aligning metrics with the profile install date. This option is useful for customizing the metrics analysis according to your specific needs.
### Time ranges
You can choose from a range of time periods to analyze metrics data, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges.
### Available filters and grouping
Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. With Adapty's metrics page, you have access to various time ranges, grouping options, and filtering possibilities.
- ✅ Filter by: Audience, attribution, country, paywall, paywall state, paywall group, onboarding, placement, country, store, product, and product store.
- ✅ Group by: Product and store.
:::note
When you filter by A/B test, cross-placement A/B tests appear as separate child tests (e.g., `My test child-0`, `My test child-1`), one per placement. See [Crossplacement A/B test limitations](ab-tests#crossplacement-ab-test-limitations) for details.
:::
You can find more information about the available controls, filters, grouping options, and how to use them for paywall or onboarding analytics in [this documentation.](controls-filters-grouping-compare-proceeds)
## Single metrics chart
One of the key components of the paywall or onboarding metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis.
The chart section on the A/B test metrics page includes a horizontal bar chart that visually represents the chosen metric values. Each bar in the chart corresponds to a metric value and is proportional in size, making it easy to understand the data at a glance. The horizontal line indicates the timeframe being analyzed, and the vertical column displays the numeric values of the metrics. The total value of all the metric values is displayed next to the chart.
Additionally, clicking on the arrow icon in the top right corner of the chart section expands the view, displaying the selected metrics on the full line of the chart.
## A/B test summary
Next to the single metrics chart, the A/B test details summary section is displayed, which includes information about the state, duration, placements, and other related details about the A/B test.
## Metrics definitions
Here are the key metrics that are available for the A/B tests:
### Revenue
Revenue represents the total amount of money generated in USD from purchases and renewals resulting from the A/B test. It includes the initial purchase and subsequent subscription renewals. The revenue metric is calculated before deducting the App Store or Play Store commission.
Learn more about [paywall](https://adapty.io/docs/paywall-metrics#revenue) and [onboarding](https://adapty.io/docs/onboarding-metrics#revenue) revenue metrics.
### CR to purchases
The conversion rate to purchases measures the effectiveness of your A/B test in converting views into actual purchases. It is calculated by dividing the number of purchases by the number of views. For example, if you had 10 purchases and 100 views, the conversion rate to purchases would be 10%.
### CR trials
The conversion rate (CR) to trials is the number of trials started from A/B test divided by the number of views. Conversion rate to trials measures the effectiveness of your A/B test in converting views into trial activations. It is calculated by dividing the number of trials started by the number of views.
### Purchases
The purchases metric represents the total number of transactions made within the paywall or onboarding resulting from the A/B test. It includes the following types of purchases:
- New purchases made.
- Trial conversions of trials that were activated.
- Downgrades, upgrades, and cross-grades of subscriptions.
- Subscription restores (e.g. when a subscription is expired without auto-renewal and is subsequently restored).
Please note that renewals are not included in the purchases metric.
### Trials
The trials metric indicates the total number of activated trials resulting from the A/B test.
### Trials cancelled
The trials canceled metric represents the number of trials in which auto-renewal has been switched off. This occurs when users manually unsubscribe from the trial.
### Refunds
Refunds for the A/B test represent the number of refunded purchases and subscriptions specifically related to the tested variations.
### Views
Views are the number of views of the paywalls or onboardings that the A/B test consits of. If the user visits two times, this will be counted as two visits.
### Unique views
Unique views are the number of unique views of the paywall or onboarding. If the user visits it two times, this will be counted as one unique view.
### Probability to be the best
The Probability to be the best metric quantifies the likelihood that a specific variant within an A/B test is the top-performing option among all the tested paywalls or onboardings. It provides a numerical probability indicating the relative performance of each paywall or onboarding. The metric is expressed as a percentage value ranging from 1% to 100%.
### ARPU (Average revenue per user)
For onboarding A/B tests only. Measures the average revenue generated from each user over a specific period. It is calculated by dividing total revenue by the number of unique users.
### ARPPU (Average revenue per paying user)
ARPPU stands for Average Revenue Per Paying User resulting from the A/B test. It is calculated as the total revenue divided by the number of unique paying users. For example, if you have generated $15,000 in revenue from 1,000 paying users, the ARPPU would be $15.
### ARPAS (Average revenue per active subscriber)
ARPAS is a metric that allows you to measure the average revenue generated per active subscriber from running the A/B test. It is calculated by dividing the total revenue by the number of subscribers who have activated a trial or subscription. For example, if the total revenue is $5,000 and you have 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber.
### Proceeds
The proceeds metric for the A/B test represents the actual amount of money received by the app owner in USD from purchases and renewals after deducting the applicable App Store / Play Store commission. It reflects the net revenue specifically associated with the variations tested in the A/B test, contributing directly to the app's earnings. For more information on how proceeds are calculated, you can refer to the Adapty [documentation.](analytics-cohorts#revenue-vs-proceeds)
### Unique subscribers
The unique subscribers metric represents the count of distinct individuals who has subscribed or activated a trial through the variations in the A/B test. It considers each subscriber only once, irrespective of the number of subscriptions or trials they initiate.
### Unique paid subscribers
The unique paid subscribers metric represents the number of unique individuals who have successfully completed a purchase and become paying subscribers through the variations in the A/B test.
### Refund rate
The refund rate for the A/B test is calculated by dividing the number of refunds specifically associated with the variations in the test by the number of first-time purchases (renewals are excluded). For instance, if there are 5 refunds and 1000 first-time purchases, the refund rate would be 0.5%.
### Unique CR purchases
The unique conversion rate to purchases for the A/B test is calculated by dividing the number of purchases specifically associated with the variations in the test by the number of unique views. For example, if there are 10 purchases and 100 unique views, the unique conversion rate to purchases would be 10%.
### Unique CR trials
The unique conversion rate to trials for the A/B test is calculated by dividing the number of trials started specifically associated with the variations in the test by the number of unique views. For example, if there are 30 trials started and 100 unique views, the unique conversion rate to trials would be 30%.
### Completions & unique completions
For onboarding A/B tests only. Completions count the number of times users complete your onboarding through the variations in the A/B test, meaning that they go from the first to the last screen. If someone completes it twice, that's two **completions** but one **unique completion**.
### Unique completions rate
For onboarding A/B tests only. The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding through the variations in the A/B test and make changes if you notice that people ignore it.
---
# File: autopilot-how-it-works
---
---
title: "Growth Autopilot: How it works"
description: "Understand the logic behind the Growth Autopilot and trust us to grow your revenue."
---
[Growth Autopilot](autopilot.md) helps you figure out which experiments to run based on your actual performance data and how similar apps in your market are doing. Instead of guessing what might work, you get specific recommendations for tests that are more likely to improve your results.
This article offers a transparent look at how Autopilot thinks — what data it uses, how it evaluates opportunities, and why certain recommendations appear. The goal is to help you feel confident using it as part of your growth workflow.
## What Autopilot actually does
Autopilot analyzes your app and finds the experiments that are most likely to increase your revenue. It looks at:
- **Your current setup**: pricing, trials, products, and how well they convert
- **Market patterns**: how similar apps structure their offers and what they charge
- **Growth potential**: which changes are most likely to make a difference
Autopilot prioritizes tests based on their potential impact and turns them into experiments you can launch right away. You get a focused plan without having to research competitors or guess what to test next.
## The data behind Autopilot
Each recommendation is built from three main data sources that work together.
#### Your app's own data
Autopilot looks at how your app performs today:
- Conversion metrics across your paywalls
- Pricing and product structure
This gives Autopilot a baseline to work from before suggesting any changes.
:::note
We don't use your app's performance data to train recommendations for other apps. Your data stays private.
:::
#### Competitor data
Autopilot compares your setup with similar apps in your market using public information like pricing, subscription structures, and common patterns in your category. This is based on third-party and public data, not private metrics from other Adapty clients.
This way, you're testing strategies that already work for apps like yours, not just random ideas. When you see the analysis, you can compare your benchmarks and competitor prices side by side. If similar apps are doing better with different pricing or structure, that's a good signal that the same approach could work for you too.
:::tip
Autopilot selects relevant competitors automatically based on what you can realistically compete with. We generally recommend sticking with these suggestions rather than adding apps that are too far ahead or too far behind. If your app falls into several categories, you might want to adjust the list to focus on the most relevant market segment.
:::
#### Industry benchmarks
Autopilot also uses category-level data to show how you compare to the industry average. This data is anonymized and aggregated, not tied to specific apps.
For example, your conversion funnel might be compared against the average for apps in your category. This helps you see if you're underperforming, doing about average, or already ahead of the curve.
#### Geographic market data
Autopilot analyzes your top-performing countries to identify where pricing adjustments could unlock more revenue. For each market, it combines:
- **Your conversion data**: How users in each country convert from install to paid compared to your global average
- **Purchasing power insights**: Real transaction data from the [Adapty Pricing Index](https://uploads.adapty.io/adapty_pricing_index.pdf) that shows how prices in different countries compare to the US benchmark
This geographic analysis helps you understand which markets are converting easily (suggesting room to increase prices) and which show price sensitivity (suggesting a need for regional pricing). Instead of applying the same pricing globally, you can tailor your strategy to each market's specific characteristics.
## How Autopilot decides what to recommend
Autopilot creates a sequence of experiments to improve your paywall step by step. Instead of testing everything at once, it focuses on one change at a time so you can see what actually works.
Here's how it works:
1. **Find the biggest opportunity**
Autopilot reviews your pricing, products, and funnel performance, then compares them with industry patterns and similar apps. It looks for where you have the most room to improve, whether that's adjusting your price, adding a trial, or changing your offer structure.
2. **Pick the next experiment**
Each recommendation is part of a testing sequence. You might test new products, price points, trial configurations, or design changes, depending on what's likely to have the biggest impact.
3. **Run winner vs. challenger rounds**
After each experiment, the winner becomes your new baseline. The next recommendation builds on that result with a new challenger. Each round gets you closer to the best setup for your app.
4. **Keep it practical**
Autopilot only suggests tests you can launch with your existing products and setup, or with small changes like creating a new product or adjusting a price. The goal is to keep testing fast and manageable.
5. **Show you the reasoning**
For each recommendation, Autopilot provides a clear hypothesis that explains exactly why this test is worth running. You'll see how your current metrics compare to competitors and industry averages, what the opportunity is, and which key metrics we expect to improve.
This turns experimentation into a repeatable process where each test teaches you something and moves you toward a more effective paywall.
## What happens after you complete the experiments
Once you finish all the recommended experiments and see an increase in revenue, your work isn't done. After some time with your new setup, you can rerun the analysis and start a fresh round of experiments. You might even choose to compete with more advanced competitors now that you've optimized your baseline. This iterative approach helps you keep maximizing your revenue as your app grows and the market evolves.
:::tip
More features are coming! Expect even smarter suggestions and the ability to launch experiments in one click. Autopilot will keep getting better at helping you grow.
:::
---
# File: maths-behind-it
---
---
title: "Maths behind the A/B tests"
description: "Understand the math behind subscription analytics for better revenue insights."
---
A/B testing is a powerful technique used to compare the performance of two different versions of a paywall or onboarding. The ultimate goal is to determine which version is more effective based on the average revenue per user over a 12-month period. However, waiting for a full year to collect data and make decisions is impractical. Therefore, a 2-week revenue per user is used as a proxy metric, chosen based on historical data analysis to approximate the target metric. To achieve accurate and reliable results, it is crucial to employ a robust statistical method capable of handling diverse data types. Bayesian statistics, a popular approach in modern data analysis, provides a flexible and intuitive framework for A/B testing. By incorporating prior knowledge and updating it with new data, Bayesian methods allow for better decision-making under uncertainty. This document provides a comprehensive guide to the mathematical analysis employed by Adapty in evaluating A/B test results and providing valuable insights for data-driven decision-making.
## Adapty's approach to statistical analysis
Adapty employs a comprehensive approach to statistical analysis in order to evaluate the performance of A/B tests and provide accurate and reliable insights. Our methodology consists of the following key steps:
1. **Metric definition:** To conduct an AB test successfully, you need to identify and define the key metric that aligns with the specific goals and objectives of the analysis. Adapty leveraged a huge amount of historical subscription app data to determine which fits the role of a proxy metric for the long-term goal of average revenue after 1 year - and it is ARPU after 14 days.
2. **Hypothesis formulation:** We create two hypotheses for the A/B test. The null hypothesis (H0) assumes that there is no significant difference between the control group (A) and the test group (B). The alternative hypothesis (H1) suggests that there is a significant difference between the two or more groups.
3. **Distribution selection:** We choose the best distribution family based on the data characteristics and the metric we observe. The most frequent choice here is log-normal distribution (taking into account zero values).
4. **Probability-to-be-best calculation:** Utilising the Bayesian approach to A/B testing, we calculate the probability to be the best option for every paywall or onboarding variant participating in the test. This value is surely connected to the p-values we used before, but it is essentially a different approach, more robust, and easier to understand.
5. **Results interpretation:** Probability to be best is exactly how it sounds. The larger the probability is, the higher the likelihood of a specific option being the best choice for the task. You need to determine the threshold for decision-making yourself, it should depend on many other factors of your specific situation, but a common probability choice is 95%.
6. **Prediction intervals:** Adapty calculates prediction intervals for the performance metrics of each group, providing a range of values within which the true population parameter is likely to fall. This helps quantify the uncertainty associated with the estimated performance metrics.
## Sample size determination
Determining an appropriate sample size is crucial for reliable and conclusive A/B test results. Adapty considers factors such as the statistical power and expected effect size, which continue to be important even under the Bayesian approach, to ensure an adequate sample size. The methods for estimating the required sample size, specific to the Bayesian approach we now employ, ensure the reliability of the analysis.
To learn more about the functionality of A/B tests, we recommend referring to our documentation on [creating](ab-tests) and [running A/B tests](run_stop_ab_tests), as well as understanding the various [A/B test metrics and results.](results-and-metrics).
Adapty's analytical framework for A/B tests now employs a Bayesian approach, but the focus remains on the definition of metrics, formulation of hypotheses, and the selection of distributions. However, instead of determining p-values, we now compute the posterior distributions and calculate the probability of each variant being the best. We also determine the prediction intervals now. This revised approach, while still comprehensive and even more robust, is designed to provide insights that are more intuitive and easier to interpret. The goal remains to empower businesses to optimize their strategies, improve performance, and drive growth based on a robust statistical analysis of their A/B tests.
---
# File: how-adapty-analytics-works
---
---
title: "How Adapty analytics works"
description: "Learn how Adapty analytics work to track subscription performance efficiently."
---
This article describes how Adapty Analytics works: what data it displays, where the data is from, and how the data is processed. It also explains the design decisions that make Adapty Analytics different, and how these decisions benefit you.
## Adapty Analytics vs Store analytics
- **Data variety**: Stores can only display their own data, and cannot access user behavior inside the app.
Adapty can combine data from multiple stores, as well as additional sources — marketing platforms and ad networks. The Adapty SDK tracks user interactions with paywalls and onboardings.
- **Update frequency**: App stores usually update their data once a day, which can limit your ability to make real-time decisions.
Adapty offers [close to real-time](#data-processing) analytics.
- **Advanced metrics**: App stores display basic metrics such as downloads, revenue, and retention rates.
Adapty also calculates advanced metrics, such as recurring revenue or average revenue per user. Dedicated sections analyze subscription issues: user churn, billing failures, etc.
- **Predictions**: Adapty uses advanced machine learning algorithms to [predict future LTV and revenue](predicted-ltv-and-revenue).
## Data and its sources
Adapty Analytics processes the following data into [charts and graphs](analytics):
- [Subscription events](events) such as
## Charts
Overview has the following charts available (you can click on the name to learn more about how we calculate it):
- [Revenue](revenue)
- [MRR](mrr)
- [ARR](arr)
- [ARPU](arppu.md)
- [ARPPU](arppu)
- [ARPAS](https://adapty.io/docs/placement-metrics#arpas)
- [Installs](installs.md)
- [New trials](new-trials)
- [New subscriptions](reactivated-subscriptions)
- [Active trials](active-trials)
- [Active subscriptions](active-subscriptions)
- [New non-subscriptions](non-subscriptions)
- [Refund events](refund-events)
- [Refund money](refund-money)
- [Subscriptions renewal canceled](cancelled-subscriptions.md)
- [Conversion rate from Install to Trial, Install to Paid, and Trial to Paid](analytics-conversion.md)
You can customize which charts to show as well as their order. To do that, press Edit in the top-right corner and then either remove charts you don't need, add more or rearrange existing ones by drag and dropping. You can also customize Overview contents in the "Add" menu:
## Controls
Controls for diving deeper into your data in Overview are very similar to what we have in [Charts](charts) — and most of them are described in [Analytics controls](controls-filters-grouping-compare-proceeds).
There is one important difference though: you can group and filter by country, store, and, most notably, by app — as Overview shows data for all of your apps at once by default. This can be helpful to understand how each of your app contributes to your business metrics:
:::note
**Timezone and install settings**
Note that these settings apply to all your apps and override what you have in [App settings](general).
- **Installs**: By default, installs are counted by `device_id`—a new installation or reinstallation on a device is counted as a separate install. You can change it by clicking **Edit**. For a detailed explanation of other options, see the [Installs definition for analytics](general#4-installs-definition-for-analytics) section.
- **Timezone**: By default, the timezone for the **Overview** page is inherited from one of your apps. If your apps have different reporting timezones, customize the Overview timezone by clicking **Edit** and selecting the appropriate option from the dropdown.
:::
---
# File: controls-filters-grouping-compare-proceeds
---
---
title: "Analytics controls"
description: "Control and filter revenue data with Adapty’s powerful analytics tools."
---
Adapty offers a wide range of controls to help you gain valuable insights and unlock the full potential of your data and gain a comprehensive view of your business performance. Whether you're analyzing charts, cohorts, funnels, retention, conversion data, or LTV, these controls provide powerful functionality. By leveraging these controls, you can dive deeper into your data and extract meaningful insights to drive your business decisions.
In the following article, you can learn more about each control and how to use them effectively. Additionally, you'll find information about which controls are supported for each type of analytics, including charts, cohorts, funnels, retention, and conversion.
| Control | Charts | Cohorts | Funnels | Retention | Conversion | LTV |
| :-------------- | :----- | :------ | :------ | :-------- | :--------- | :-- |
| Time ranges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Data comparison | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Filtering | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Grouping | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Chart views | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Table view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| CSV data export | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Proceeds | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Taxes | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
### Time ranges
When using the Adapty calendar to set a time range for a chart, you have several convenient options to choose from. These options determine the date range displayed on the x-axis of the charts, allowing you to focus on specific periods of time. Here are the available quick options for time ranges:
- **Last 7 days**: Displays data for the most recent 7-day period.
- **Last month**: Shows data from the current date to the same day in the previous month.
- **Last 28 days**: Useful for tracking weekly subscription products, as it covers the last four weeks.
- **Last 3 months**: Displays data from the current date to three months ago.
- **Last 6 months**: Shows data from the current date to six months ago.
- **Last year**: Displays data from the current date to one year ago.
- **Previous month**: Covers the full calendar month before the current month.
- **This month**: Shows data from the 1st day of the current month until today.
- **This quarter**: Displays data from the 1st day of the current quarter until today.
- **This year**: Covers data from the 1st day of the current year until today.
In addition to these predefined options, you can also select the **Custom** option to set a specific time period of your choice. This flexibility allows you to analyze your data in more granular detail or focus on specific events or campaigns.
Apart from selecting specific time ranges, you can also adjust the time scale of charts. By choosing a day timescale, you can view the most detailed level of data, while opting for lower resolutions such as week, month, quarter, or year, allows you to identify longer-term trends.
For charts, the time scale determines the scale of the grid on the x-axis and the resolution at which the data is displayed. The available time scale options include day, week, month, quarter, or year. Choosing a specific time scale allows you to view the data at different resolutions, helping you identify shorter-term or longer-term trends.
When analyzing cohorts in Adapty, you choose the cohort length, which determines the size of each cohort and the grouping of users based on a specific time period. The time frame in cohorts analysis refers to the duration for which you want to analyze user behavior, and it helps define the boundaries of the cohorts.
In LTV (lifetime value) analysis, the cohort length is chosen in the grouping settings and determines the time period over which you want to calculate the lifetime value of users. Similar to cohorts analysis, the cohort length in LTV analysis is independent of the time frame displayed on the x-axis of the charts.
We have 2 formats of date and time - American and European. You can set one of them in your Adapty account as described [here](account).
Please also note that all charts in Adapty analytics are displayed in UTC time.
### Data comparison
To analyze the dynamics of your app's metrics, you can utilize the comparison feature located next to the calendar. It offers a convenient way to compare your metrics with the previous period, although you also have the flexibility to customize the comparison range based on your specific requirements.
Here's how you can interpret the insights provided by the comparison feature:
- **Comparison Display:** After selecting the comparison period, you can toggle between displaying the comparison on the chart or as a numerical value only.
- **Difference Indicator:** The comparison shows the variance between your current result and the result from the previous period. Higher values are indicated in green, while lower values are indicated in red.
- **Chart Visualization:** If you have no grouping or only one grouping selected, the comparison will be displayed on the chart as well. You can choose from different chart types such as area, line, or column to visually highlight the differences.
- **Detailed Tooltip:** Hovering over the chart will reveal a tooltip with additional details, allowing you to examine the specifics of the comparison.
- **Multiple Grouping and Comparison:** If you have multiple grouping options enabled, you can view multiple comparisons simultaneously on a single chart. This feature is available specifically for column charts.
### Filtering and grouping
Filters play a crucial role in refining the data displayed in charts by including only the information that matches specific attributes. This feature becomes especially handy when you wish to examine the performance of a particular property, such as a specific country or product identifier.
By grouping chart data, you can analyze the individual components that make up the chart totals. This is particularly valuable when you want to evaluate the performance of particular properties.
It's important to note that certain charts may not support every type of filtering or grouping. To determine the compatibility of filters and grouping options with each chart, you can refer to the corresponding information provided on the respective chart description page.
In Advanced Analytics, you have access to the following filtering and grouping options, empowering you to refine your data analysis:
| | Filtering | Grouping | Description |
|:---------------------|:----------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Attribution | ✅ | ✅ | Filter or group metrics by Attribution fields like Status, Channel, Campaign, Adgroup, Adset, and Creative. To be able to filter/group data by **Attribution**, you need to set up the [attribution integration](attribution-integration.md) first. |
| Renewal Status | ❌ | ✅ | Group metrics by the product's renewal status, which indicates whether or not the subscription will be renewed in the next period. |
| Period | ✅ | ✅ | Filter or group metrics by the subscription period. |
| Country | ✅ | ✅ | Filter or group metrics by user's store country (if available, otherwise country is inferred using currency code or device's IP). |
| Offer Type | ✅ | ❌ | Filter metrics by the type of applied offer. Available offer types include:
### Chart views
The Analytics section provides you with the flexibility to view each chart in different visual representations, such as stacked column, stacked area, line, 100% stacked column, and 100% stacked area. By selecting the appropriate view, you can effectively communicate the information displayed on the chart and enhance your data analysis experience.
To change the view of a chart, simply locate and select the view dropdown menu, and then choose the desired representation option
### Table view
In addition to the chart view, Adapty also provides a table view for each chart. The table view presents the underlying data used to generate the chart in a tabular format, allowing users to view and analyze the data in a more granular way. The table view is a useful tool for users who prefer to work with data in a more structured format or need to export the data for further analysis outside of Adapty.
### CSV data export
To analyze the raw data behind charts, cohort analyses, funnels, retentions, or conversion analytics, you can export it in CSV format by clicking the **Export** button.
You can also [retrieve the same data via the API](export-analytics-api). Regardless of the method, the data file will be identical.
This feature gives you access to the underlying data, which you can further analyze in spreadsheet applications or other tools to gain deeper insights.
### Store commission and taxes
One crucial aspect of revenue calculation is the inclusion of taxes (which can vary based on the user's store account country) and store commission fees. Adapty currently supports commission and tax calculation for both App Store and Play Store.
Adapty automatically calculates the applicable store commission for each transaction, accounting for various commission structures set by the App Store and Play Store. These include the Small Business Program (15% commission), reduced rates for long-term subscriptions (15% after one year of continuous renewal), country-specific rates (such as 26% in Japan), and standard rates (up to 30%). For detailed information on how Adapty calculates store commission fees, please refer to the corresponding documentation for [App Store](app-store-small-business-program) and [Play Store.](google-reduced-service-fee)
In the charts tab of the Analytics section, Adapty introduces a dropdown field with three display options.
The dropdown allows you to choose how the revenue is displayed in the chart. The available options are as follows:
#### Gross revenue
This option displays the total revenue, including taxes and commission fees from both App Store / Play Store. It represents the complete revenue generated by transactions before any deductions.
#### Proceeds after store commission
This option displays the revenue amount after deducting the store commission fee.
It represents the revenue that remains after the App Store / Play Store cuts its commission fees from the gross revenue. Taxes are not deducted in this display option.
Adapty automatically applies the appropriate commission rate for each transaction based on various factors. For detailed information on how Adapty calculates store commission fees, including the Small Business Program, long-term subscription discounts, country-specific rates, and other applicable commission structures, please refer to the corresponding documentation for [App Store](app-store-small-business-program) and [Play Store](google-reduced-service-fee), as well as the [Store commission and taxes](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) section above.
#### Proceeds after store commission and taxes
This option displays the revenue amount after deducting both the store commission fee and taxes.
It represents the net revenue received by the app after accounting for both store's commission and applicable taxes. We consider the VAT rate of the user's store account country when calculating taxes. Please consider that Adapty follows the logic that for Apple taxes are applied to the post-commission revenue from a transaction, while Google applies taxes to the full amount (before store commissions are reduced from the revenue).
It's important to note that this revenue dropdown applies to various revenue-related charts, such as [Revenue](revenue), [MRR](mrr) (Monthly recurring revenue), [ARR](arr) (Annual recurring revenue), [ARPU](arpu) (Average revenue per user), and [ARPPU](arppu) (Average revenue per paying user). These charts provide valuable insights into revenue-related information for your app.
---
# File: revenue
---
---
title: "Revenue"
description: "Track and analyze your app’s revenue using Adapty’s subscription insights."
---
The revenue chart displays the total revenue earned from both subscriptions and one-time purchases, minus the revenue that was refunded later. The Revenue chart is an essential tool to monitor the financial performance of the app.
### Calculation
The Adapty revenue chart calculates the total revenue generated by the app minus the refunded revenue. This includes both new revenue, which comes from a customer's first paid transaction, such as new paid subscriptions, initial non-consumable and consumable purchases, and trial-to-paid conversions, as well as renewal revenue, which comes from subsequent paid transactions such as paid subscription renewals, promotional offers for existing subscribers, additional non-renewing subscription purchases, and more.
The calculation is done before deducting the store's fees and taxes. Revenue from each transaction is attributed to the period in which the transaction occurred, which can lead to period-over-period fluctuations based on the mix of subscription durations being started or renewed. It's important to note that refunds are treated as negative revenue and attributed to the day they occurred on (not to the day the subscription began).
Revenue = Total amount billed to customers - Revenue from refunded purchases/subscriptions.
For example, there were 5 monthly $10 subs, 1 yearly $100 sub, and 10 one-time $50 purchases today,
revenue = 5_$10 + 1_$100 + 10\*$50 = $650
After calculating the total revenue earned from in-app purchases, Adapty's Revenue chart provides an estimate of the expected proceeds by deducting the store's commission fee and taxes. The commission fee is calculated based on the gross revenue, while taxes are deducted before the commission fee is applied. This ensures that the displayed proceeds reflect the net revenue received by the app after accounting for both the store's commission and applicable taxes. To learn more about how Adapty calculates commission fees, taxes, and estimates the expected proceeds, refer to the relevant [documentation.](controls-filters-grouping-compare-proceeds#store-commission-and-taxes)
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Revenue chart usage
The Revenue chart is a valuable tool to track the financial performance of the app. By analyzing the chart's data, you can gain insight into the growth trajectory of your app over time. One useful approach is to switch to a monthly resolution and observe the last 12 months of revenue to assess the app's overall trend. It is also helpful to analyze the mix of new and renewal revenue to identify where growth is coming from. You can further segment the data by different dimensions, such as product or user type, to gain deeper insights into the app's financial performance. Overall, the Revenue chart is an essential metric for understanding the financial health of an app and optimizing its revenue strategy.
### Similar metrics
In addition to Revenue, Adapty also provides metrics for other revenue-related events, such as MRR, ARR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides:
- [MRR](mrr)
- [ARR](arr)
- [ARPU](arpu)
- [ARPPU](arppu)
---
# File: mrr
---
---
title: "MRR"
description: "Understand and optimize Monthly Recurring Revenue (MRR) in Adapty."
---
The Monthly recurring revenue (MRR) chart displays the normalized revenue generated by your active paid subscriptions on a monthly basis. This chart enables you to understand your business's velocity and size, regardless of the fluctuations that may arise from varying subscription durations.
### Calculation
Adapty calculates the predictable and recurring revenue components of your subscription business using the following formula:
Where:
Ps - subscription price
Ns - number of active paid subscriptions for this subscription. Adapty considers any paid subscription that has not yet expired as an active subscription.
Dsm - subscription duration in months (0.23 for weekly subscriptions)
It is important to note that Adapty does not include non-recurring subscriptions, consumable, or one-time purchases in the calculation of Monthly Recurring Revenue (MRR). This is because these types of purchases do not represent predictable and recurring revenue.
Basically, MRR shows revenue from all active subscriptions normalized to one month. For example, for a yearly subscription, instead of counting full revenue from the start, revenue is split into 12 equal parts which are evenly spread across 12 month period.
MRR excludes subscriptions that have been refunded. When a subscription is refunded, it's removed from the MRR calculation for all periods it was active, ensuring that MRR reflects only the recurring revenue from subscriptions that remain valid.
E.g. if there are 2 active yearly subscriptions with price $240 and 10 monthly subscriptions with a price $30,
MRR = (2 \* $240 / 12) + (10 \* $30 / 1) + (20 \* $10 / 0.23) = $1209.5
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution Ad group, attribution Ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### MRR chart usage
MRR is a crucial metric for businesses that rely on recurring subscription revenue. It not only captures the size of your subscriber base but also standardizes different subscription durations to a common denominator (monthly recurring revenue). By doing so, MRR provides a real velocity metric for your business, making it easier to track your growth trajectory accurately.
To leverage MRR effectively, segment your subscriber cohorts by their first purchase month and change the resolution to monthly. By doing this, you can create a stacked area chart that reveals how monthly subscriber cohorts have translated over time. This approach enables you to identify trends and patterns in your subscriber base, making it easier to adjust your business strategy and optimize your products and marketing efforts accordingly.
### Similar metrics
In addition to MRR, Adapty also provides metrics for other revenue-related events, such as Revenue, ARR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides:
- [Revenue](revenue)
- [ARR](arr)
- [ARPU](arpu)
- [ARPPU](arppu)
---
# File: arr
---
---
title: "ARR"
description: "Track Annual Recurring Revenue (ARR) and optimize your subscription strategy."
---
The Annual recurring revenue chart shows revenue from all active auto-renewable subscriptions normalized to one year. The chart considers any paid, unexpired subscription as active. ARR is a crucial metric for tracking your subscription business's growth and predicting future revenue.
### Calculation
The Adapty ARR chart calculates the total revenue generated by the app. Any paid, unexpired subscription is considered active. ARR includes normalized annual revenue from all active paid subscriptions – even if their auto-renew status is currently disabled. This also means non-recurring subscriptions, consumable, or one-time purchases included in ARR calculation. The metric is calculated before the store's fee.
ARR = sum of ( (Ps \* Ns / Dsy)
where Ps - subscription price,
Ns - number of active paid subscriptions for this subscription,
Dsy - subscription duration in years (1/12 for monthly and ~1/52 for weekly subscriptions).
This metric is only useful when annual subscriptions are the biggest chunk of your sales.
For example, there are 2 active annual subscriptions with a price of $240, and 10 monthly subscriptions with a price of $30, and 20 weekly subscriptions with a price of $10,
ARR = (2 _ $240 / 1) + (10 _ $30 / (1/12)) + (20\*$10 / (1/52)) = $14480
:::note
ARR excludes subscriptions that have been refunded. When a subscription is refunded, it's removed from the MRR calculation for all periods it was active, ensuring that MRR reflects only the recurring revenue from subscriptions that remain valid.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### ARR chart usage
The Annual Recurring Revenue (ARR) chart is a valuable tool for measuring the growth and scale of your subscription-based business. It's a widely-used metric that provides a normalized view of your recurring revenue over a 12-month period. To gain a better understanding of what's driving your ARR, you can segment your data by key subscriber segments such as Store or Product Duration. By doing so, you can identify which segments are driving the most revenue, and optimize your business strategy accordingly.
### Similar metrics
In addition to the ARR chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides:
- [Revenue](revenue)
- [MRR](mrr)
- [ARPU](arpu)
- [ARPPU](arppu)
---
# File: arpu
---
---
title: "ARPU"
description: "Analyze Average Revenue Per User (ARPU) to optimize revenue generation."
---
ARPU (average revenue per user) chart displays the average revenue generated per user for a given period. This metric is calculated by dividing the total revenue generated by a cohort of customers by the number of users in that cohort.
### Calculation
Adapty calculates the ARPU chart by dividing the total revenue earned over a given period by the number of non-unique users (installs) during the same period.
ARPU = Revenue / Number of new users
For instance, if your app generated $10,000 in revenue over the course of a week, and had 2,000 non-unique users during that same period, the ARPU for that week would be $5 ($10,000/2,000).
The calculation is done before the store's fee, and the refund amount is excluded from the revenue.
### Available filters and grouping
- ✅ Filter by: Attribution, country, and store.
- ✅ Group by: Country, store, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### ARPU chart usage
ARPU chart usage is beneficial for businesses to track their overall revenue generation and understand how much revenue is being generated per user. By analyzing the ARPU chart, businesses can identify trends, patterns, and areas of improvement to optimize their revenue generation strategies. This can help businesses make data-driven decisions to increase their user engagement, target their marketing efforts, and improve their overall monetization strategy.
### Similar metrics
In addition to the ARPU chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARR, and ARPU. To learn more about these revenue-related metrics, please refer to the following documentation guides:
- [Revenue](revenue)
- [MRR](mrr)
- [ARPPU](arppu)
- [ARR](arr)
---
# File: arppu
---
---
title: "ARPPU"
description: "Understand ARPPU (Average Revenue Per Paying User) and how it impacts your app’s monetization."
---
### ARPPU
The Average revenue per paying user (ARPPU) chart displays the average revenue per paid user. It displays the actual revenue generated by paying customers, divided by the number of customers, minus refunds.
### Calculation
The ARPPU chart is calculated as:
ARPPU = Revenue / Number of users who paid
For example, if your app generated $10,000 in revenue over a given period and had 500 paying customers during that time, the ARPPU would be calculated as $10,000 / 500 = $20. This means that on average, each paying customer generated $20 in revenue during the selected period.
It's important to note that the ARPPU value displayed represents the sum of total revenue divided by the total number of paying customers. The calculation is done before the store's fee and the refund amount is excluded from the revenue. As one user can pay more than one time during the whole period, the total ARPPU value may be higher than the daily value of ARPPU. This metric provides valuable insights into the revenue-generating capabilities of your app's paying user base and can help you optimize your pricing and subscription strategy to maximize revenue generation.
:::note
Refunds reduce the revenue component of this calculation, resulting in a lower ARPPU value. However, the user count in the denominator includes users whose purchases were later refunded, which can further decrease the average. This metric shows the true average revenue after accounting for refunds.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### ARPPU chart usage
The ARPPU chart in Adapty is a powerful tool that can help businesses understand the revenue generated by each paying user. By analyzing this metric over time, businesses can identify which channels, networks, and campaigns attract the most valuable customers. This information can be used to optimize marketing strategies and increase revenue by targeting high-value customers. With Adapty's ARPPU chart, businesses can make data-driven decisions that improve their bottom line and drive long-term success.
### Similar metrics
In addition to the ARPPU chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARR, and ARPU. To learn more about these revenue-related metrics, please refer to the following documentation guides:
- [Revenue](revenue)
- [MRR](mrr)
- [ARPU](arpu)
- [ARR](arr)
---
# File: installs
---
---
title: "Installs"
description: "Track app installs and understand their impact on subscriptions with Adapty."
---
The Installs chart shows the total number of app installations.
### Calculation
The Installs chart counts how many times your app has been installed.
Incomplete installations or downloads that are canceled before completion are not counted.
You can choose how installs are defined for analytics in **App Settings** under
[**Installs definition for analytics**](general#4-installs-definition-for-analytics).
This setting determines what is considered a new install event.
The available options differ in how installs are grouped:
- **By device installations** — each app installation on a device is counted as a separate install, including reinstalls.
An install represents a completed app installation from the store. Profile creation (on SDK activation or user logout), user authentication, and app upgrades do not generate additional install events.
- **By unique users** — only the first installation associated with an identified user is counted; installations on additional devices are ignored. Use this setting only if you identify users in Adapty. Note that app stores and attribution platforms (such as App Store Connect, Google Play Console, and AppsFlyer) use a device-based approach to counting installs. If you count installs by customer user IDs in Adapty, install numbers may differ from these external services.
Because a single user may install the app on multiple devices, switching between these options can change install counts and conversion metrics.
If you are using the legacy option based on profiles, installs are calculated using a profile-based approach, which may result in higher install counts compared to device- or user-based definitions.
### Available filters and grouping
- ✅ Filter by: Attribution, country, and store.
- ✅ Group by: Country, store, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Installs chart usage
The Installs chart provides a useful metric to track the overall growth of the user base. By analyzing the chart, you can gain insights into the number of new users who have installed their app for the first time, as well as any reinstalls by existing users. This information can help to identify trends and patterns in user acquisition over time, and make informed decisions about marketing and promotional activities.
---
# File: active-subscriptions
---
---
title: "Active subscriptions"
description: "Monitor and manage active subscriptions with Adapty's robust analytics."
---
The Active subscriptions chart displays the amount of unique paid subscriptions that have not yet expired at the end of each selected period. It includes regular (unexpired) in-app subscriptions that started and are currently active and excludes both free trials and subscriptions with canceled renewals.
### Calculation
Adapty's Active Subscriptions calculation logic counts the number of paid, unexpired subscriptions at the end of a given period. At a daily resolution, the amount of Active subscriptions represents the number of unexpired subscriptions at the end of that day. Therefore, the count of Active subscriptions for a given day indicates the number of unexpired subscriptions at the end of that day. At a monthly resolution, the count of Active Subscriptions represents the number of unexpired subscriptions at the end of that month. Note that, a paid subscription without a grace period is considered expired once its next renewal date has passed without a successful renewal.
For example, if there were 500 active subscriptions at the end of last month, 50 new subscriptions were started this month, and 25 subscriptions expired this month, then there are 525 active subscriptions at the end of this month.
:::note
When a subscription is refunded, it's subtracted from the active subscriptions count. This ensures the metric accurately reflects subscriptions that are currently generating revenue for your app.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Active subscriptions chart usage
The Active subscriptions chart is useful to get valuable insights into the number of recurring, individual paid users from your app. This metric serves as a proxy for the size and growth potential of a business. Combining Active Subscriptions with filters and grouping helps you to gain a deeper understanding of their paid subscriber base composition, making it a powerful tool for data analysis.
### Similar metrics
In addition to Active subscriptions, Adapty also provides metrics for other subscription-related events, such as new subscriptions, subscriptions renewal canceled, expired subscriptions, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation:
- [Churned (expired) subscriptions](churned-expired-subscriptions)
- [Cancelled subscriptions](cancelled-subscriptions)
- [Non-subscriptions](non-subscriptions)
---
# File: reactivated-subscriptions
---
---
title: "New subscriptions"
description: "Track and manage reactivated subscriptions to optimize user retention."
---
The New subscriptions chart displays the amount of new (first-time activated) subscriptions in your app. This metric shows the number of new subscriptions starting in a specific time period, including both subscriptions that start from scratch and free trials that convert into paid subscriptions. It does not include subscription renewals or subscriptions that have been restarted.
### Calculation
Adapty's calculation logic for the New subscriptions chart counts the number of new (first-time activated) subscriptions during a given period, including both subscriptions that start from scratch and free trials that convert into paid subscriptions. At a daily resolution, the count of new subscriptions represents the number of new subscriptions activated on that day. Therefore, the count of new subscriptions for a given day indicates the number of new subscriptions activated on that day. At a monthly resolution, the count of new subscriptions represents the number of new subscriptions activated during that month.
:::note
The New subscriptions count includes all subscriptions that were initially activated during the period, even if they were later refunded. This means the displayed count may be higher than the number of subscriptions that ultimately generated revenue. To understand the net impact, compare this metric with the Refund events chart.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, renewal status, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### New subscriptions chart usage
The new subscriptions chart provides valuable insights into the number of newly acquired, individual paid users for your app. This metric is crucial for assessing the growth potential of your business. By applying filters and grouping to the new subscription data, you can enhance your understanding of the composition of your new subscriber base. This chart serves as a powerful tool for in-depth data analysis in terms of new user acquisition.
### Similar metrics
In addition to New subscriptions, Adapty also provides metrics for other subscription-related events, such as active subscriptions, subscriptions renewal canceled, expired subscriptions, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides:
- [Active subscriptions](active-subscriptions)
- [Churned (expired) subscriptions](churned-expired-subscriptions)
- [Cancelled subscriptions](cancelled-subscriptions)
- [Non-subscriptions](non-subscriptions)
---
# File: non-subscriptions
---
---
title: "Non-subscriptions"
description: "Learn how to manage non-subscription products in Adapty and track user purchases efficiently."
---
The Non-subscriptions chart displays the number of in-app purchases such as consumables, non-consumables, and non-renewing subscriptions. The chart doesn't include renewable payments. The chart shows the total count of these types of in-app purchases and can help you track user behavior and engagement over time.
### Calculation
Adapty's calculation logic for the Non-subscriptions chart involves counting the number of in-app purchases made by users that are classified as consumables, non-consumables, and non-renewing subscriptions. This chart excludes renewable payments such as auto-renewing subscriptions.
- Consumables are items that users can purchase multiple times, such as fish food in a fishing app, or extra in-game currency.
- Non-consumables are items that users can purchase once and use forever, such as a race track in a game app, or ad-free versions of an app.
- Non-renewing subscriptions are subscriptions that expire after a set period of time and do not renew automatically, such as a one-year subscription to a catalog of archived articles. The content of this in-app purchase can be static, but the subscription will not renew automatically once it expires.
:::note
This chart counts only purchase events and does not subtract refunded purchases. If you have non-subscription products that are frequently refunded, the displayed count will be higher than the actual number of purchases that generated revenue.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, and store.
- ✅ Group by: Product, country, store, paywall, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Non-subscriptions chart usage
The Non-subscriptions chart is an important tool for app developers to gain insights into the types of in-app purchases made by users, including consumables, non-consumables, and non-renewing subscriptions. By tracking this metric over time, you can better understand user behavior and engagement with their app. Using the Non-subscriptions chart with filters and grouping, you can dive deeper into their users' purchase patterns and preferences, helping them to optimize pricing strategies and improve overall user satisfaction. The Non-subscriptions chart is an essential tool for app to make data-driven decisions that ultimately lead to a better user experience and increased revenue.
### Similar metrics
In addition to non-subscriptions, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, subscriptions renewal canceled, and expired subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides:
- [Active subscriptions](active-subscriptions)
- [New subscriptions](reactivated-subscriptions)
- [Churned (expired) subscriptions](churned-expired-subscriptions)
- [Cancelled subscriptions](cancelled-subscriptions)
---
# File: cancelled-subscriptions
---
---
title: "Subscriptions renewal cancelled"
description: "Handle cancelled subscriptions efficiently with Adapty’s management tools."
---
The Subscriptions renewal canceled chart displays the number of subscriptions that have had their auto-renew status switched off (canceled by the user). When a subscription's auto-renew status is turned off, it means that the subscription will not renew automatically for the next period. However, the user still retains access to the app's premium features until the end of the current period.
### Calculation
Adapty's calculation logic for the subscriptions renewal cancelled chart involves counting the number of subscriptions that have had their auto-renewal status switched off during a given period. This includes subscriptions that were canceled by the user and will not renew automatically for the next period.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Subscriptions renewal canceled chart usage
The Subscription renewal canceled chart is useful to get valuable insights into the number of recurring, individual paid users from your app. This metric serves as a proxy for the size and growth potential of a business. Combining active subscriptions with filters and grouping helps you to gain a deeper understanding of their paid subscriber base composition, making it a powerful tool for data analysis.
### Similar metrics
In addition to Subscription renewal canceled chart, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, expired subscriptions, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides:
- [Active subscriptions](active-subscriptions)
- [Churned (expired) subscriptions](churned-expired-subscriptions)
- [New subscriptions](reactivated-subscriptions)
- [Non-subscriptions](non-subscriptions)
---
# File: churned-expired-subscriptions
---
---
title: "Churned (expired) subscriptions"
description: "Manage churned and expired subscriptions to improve user retention."
---
The churned (expired) subscriptions chart displays the number of subscriptions that have expired, meaning that the user no longer has access to the premium features of the app. Typically, this occurs when the user decides to stop paying at the end of the subscription period for the app or encounters a billing issue.
### Calculation
The churned (expired) subscriptions chart calculation logic for Adapty involves counting the number of subscriptions that have expired during a given period. This includes users who have decided to stop paying for the app or those who have experienced billing issues. To obtain this chart, the number of expired subscriptions should be counted daily or monthly. At a daily resolution, the count of expired subscriptions represents the number of subscriptions that expired on that day, while at a monthly resolution, it represents the number of expired subscriptions during that month.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Expiration reason, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Churned subscriptions chart usage
The Churned (expired) subscriptions chart is a useful metric to gain insights into the number of users who have stopped paying for the app or have experienced billing issues during a specific period. This metric provides information on the number of users who have churned, which can be used to identify trends in user behavior and billing issues. By combining the Churned subscription chart with filters and grouping, app developers or business owners can gain a deeper understanding of their user base and analyze the reasons for churn.
### Similar metrics
In addition to Churned subscriptions, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, subscriptions renewal canceled, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides:
- [Active subscriptions](active-subscriptions)
- [New subscriptions](reactivated-subscriptions)
- [Cancelled subscriptions](cancelled-subscriptions)
- [Non-subscriptions](non-subscriptions)
---
# File: active-trials
---
---
title: "Active trials"
description: "Track and manage active subscription trials with Adapty analytics."
---
The active trials chart in Adapty displays the number of unexpired free trials that are currently active at the end of a given period. Active means subscriptions that have not yet expired; therefore, users still have access to the paid features of the app.
### Calculation
Adapty calculates the number of active trials in a given period by referring to the count of unexpired free trials by the end of that period. This count remains unchanged until the trial expires, regardless of its auto-renew status. At a daily resolution, the count of Active Trials represents the number of unexpired trials by the end of that day.
For example, if 100 trials were active yesterday, 10 new trials were activated today, and 5 trials have expired today, then there are 105 active trials today.
However, at a monthly resolution, the count of Active Trials represents the number of unexpired trials by the end of that month.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
### Active trials chart usage
This chart provides valuable insights into the effectiveness of your app's trial offers and allows you to monitor the number of users currently taking advantage of your free trial periods. By leveraging the insights provided by the Active Trials chart, you can optimize your app's free trial strategy and maximize user engagement and revenue generation. With Adapty's powerful analytics and monitoring tools, you'll have everything you need to make data-driven decisions that drive your app's success.
### Similar metrics
In addition to Active Trials, Adapty also provides metrics for other trial-related events, such as New trials, Trial Renewal cancelled, and Expired trials. To learn more about these trial-related metrics, please refer to the following documentation:
- [New trials](new-trials)
- [Trial renewal cancelled](trials-renewal-cancelled)
- [Expired trials](expired-churned-trials)
---
# File: new-trials
---
---
title: "New trials"
description: "Manage new subscription trials and optimize trial-to-paid conversion rates."
---
The new trial chart displays the number of activated trials during the selected time period.
### Calculation
Adapty's calculation of the new trials refers to the number of trials initiated during a specific period. Adapty tracks the number of trials started within the selected period, regardless of their status (expired or active) at the end of the period.
For example, if you select a monthly period and 50 users start a trial during that month, then the number of new trials initiated during that period would be 50. Similarly, if you choose a daily resolution, Adapty tracks the number of trials started each day, regardless of their status at the end of the day.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in[ this documentation](controls-filters-grouping-compare-proceeds).
### New trials chart usage
The New trials chart is a powerful tool for tracking the effectiveness of your app's promotional campaigns and user acquisition efforts. For instance, if you run a targeted ad campaign on social media or search engines, you can use the New trials chart to monitor the number of new trials initiated during the campaign period. By analyzing this data, you can determine the effectiveness of the campaign and make data-driven decisions to optimize your promotional strategies in the future.
### Similar metrics
In addition to New Trials, Adapty also provides metrics for other trial-related events, such as Active trials, Trial renewal cancelled, and Expired trials. To learn more about these trial-related metrics, please refer to the following documentation:
- [Active trials](active-trials)
- [Trial renewal cancelled](trials-renewal-cancelled)
- [Expired trials](expired-churned-trials)
---
# File: trials-renewal-cancelled
---
---
title: "Trials renewal cancelled"
description: "Understand trial renewals, cancellations, and subscription flows with Adapty’s insights."
---
The Trials renewal cancelled chart displays the number of trials with cancelled renewal (cancelled by user). When the renewal for the trial is disabled, this means that this trial won't be automatically converted to a paid subscription, yet the user still has premium features of the app until the end of the current period.
### Calculation
Adapty calculates the number of Trials renewal cancelled during a specific period by counting the trials that users have cancelled before the end of their trial period. This count remains unchanged regardless of the trial's auto-renewal status.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds)
### Trials renewal cancelled chart usage
The Trials renewal cancelled chart is a useful tool that provides insights into the trial cancellation patterns of your app's users. By monitoring the number of users who have cancelled their trial subscriptions during a specific period, you can optimize your app's trial offerings and enhance user satisfaction. By leveraging the insights provided by this chart, you can determine whether your trial strategy needs adjustment to encourage users to continue their subscriptions.
### Similar metrics
In addition to the Trials renewal cancelled chart, Adapty also provides metrics for other trial-related events, such as New trials, Active trials, and Expired trials. To learn more about these trial-related metrics, please refer to the following documentation:
- [New trials](new-trials)
- [Active trials](active-trials)
- [Expired trials](expired-churned-trials)
---
# File: expired-churned-trials
---
---
title: "Expired (churned) trials"
description: "Manage expired and churned trials effectively with Adapty analytics."
---
The Expired (churned) trials chart displays the number of trials that have expired, leaving users without access to the app's premium features. In most cases, this occurs when users decide not to pay for the app or experience billing issues.
### Calculation
Adapty calculates the number of "Expired (Churned) Trials" during a specific period by counting the number of trials that have ended and users no longer have access to premium app features. This count remains unchanged regardless of the reason for the trial's expiration, such as a user's decision not to pay for the app or billing issues.
In addition, Adapty allows you to group the chart by expiration reason, such as user-initiated cancellations or billing issues. This grouping provides valuable insights into why users are churning and enables you to take proactive measures to prevent churn and optimize your app's success.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Expiration reason, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds)
### Expired (churned) trials chart usage
The Expired (churned) trials chart is a valuable tool that provides insights into the number of trial periods that have ended, leaving users without access to premium app features. By monitoring the number of users who have churned during a specific period, you can identify patterns in user behavior and billing issues, and take proactive measures to reduce churn and improve user retention. By leveraging the insights provided by this chart, you can adjust your app's strategy and billing practices to encourage users to continue their subscriptions.
### Similar metrics
In addition to the Trials renewal canceled chart, Adapty also provides metrics for other trial-related events, such as New Trials, Active Trials, and Expired Trials. To learn more about these trial-related metrics, please refer to the following documentation:
- [New trials](new-trials)
- [Active trials](active-trials)
- [Trials renewal canceled](trials-renewal-cancelled)
---
# File: refund-events
---
---
title: "Refund events"
description: "Manage refund events in Adapty to reduce churn and optimize revenue."
---
The Refund events chart shows how many purchases and subscriptions were refunded. Adapty ties each refund event to the date the refund was issued, not to the subscription start date.
### Calculation
Adapty counts every purchase or subscription refunded within the selected period. Each refund is attributed to the date it happened, not to when the subscription began. Refunds for trials are excluded because trials generate no revenue.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, duration, and refund reason.
- ✅ Group by: Product, country, store, paywall, duration, refund reason, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in the [detailed guide](controls-filters-grouping-compare-proceeds).
### Refund events chart usage
Use the Refund events chart to spot refund spikes and recurring issues. Tracking refunds over time helps you detect patterns and take actions that reduce churn and protect revenue.
### Similar metrics
Adapty also tracks other issue-related events—Refund money, Billing issue, and Grace period. See their documentation:
- [Refund money](refund-money)
- [Billing issue](billing-issue)
- [Grace period](grace-period)
---
# File: refund-money
---
---
title: "Refund money"
description: "Learn how to process refunds for subscriptions in Adapty without revenue loss."
---
The Refund money chart shows the amount refunded during the selected period. Adapty ties each refund event to the date it was issued, so revenue decreases in that same period.
### Calculation
Adapty counts only revenue-generating transactions—new paid subscriptions, renewals, and one-time purchases. Free trials, which produce no revenue and cannot be refunded, are excluded. Each refund amount is tied to the date it was processed, so the revenue decrease appears in that same period.
:::info
The refund amount is calculated before the store's fee is deducted.
:::
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, duration, and refund reason.
- ✅ Group by: Product, country, store, paywall, duration, refund reason, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
Learn more about controls, filters, and grouping in the [detailed guide](controls-filters-grouping-compare-proceeds).
### Refund Money chart usage
Use the Refund money chart to track the financial impact of refunds. Watching refund amounts over time helps you spot patterns and adjust your product or marketing strategies to reduce refund requests and protect revenue.
### Refund request management
The Refund saver helps Adapty users handle refund requests from Apple's App Store more efficiently through automation. It saves time and reduces revenue loss by streamlining the process. With real-time notifications and actionable insights, this tool makes it easier to address refund requests while staying compliant with Apple's guidelines.
Learn more about [Refund saver](refund-saver.md).
### Similar metrics
In addition to the Refund money chart, Adapty also tracks other issue-related events—Billing events, Billing issue, and Grace period. For details, see:
- [Refund events](refund-events)
- [Billing issue](billing-issue)
- [Grace period](grace-period)
---
# File: grace-period
---
---
title: "Grace period"
description: "Understand how subscription grace periods work and improve user retention."
---
The Grace period chart displays the number of subscriptions that have entered the grace period state due to a [billing issue](billing-issue). During this period, the subscription remains active while the store tries to receive payment from the subscriber. If payment is not successfully received before the grace period ends, the subscription enters the billing issue state.
### Calculation
Adapty calculates the Grace period chart by tracking the number of subscriptions that have entered the Grace period state within a given time period. The Grace period begins when the subscription enters the billing issue state due to a payment failure and ends after a specified amount of time (6 days for weekly subscriptions, 16 days for all other subscriptions) or when payment is successfully received. The chart provides insights into the effectiveness of the grace period feature and can help identify potential issues with payment processing or subscription management.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds)
### Similar metrics
In addition to the Grace period chart, Adapty also provides metrics for other issues-related events, such as Refund events, Refund money, and Billing issue. To learn more about these issue-related metrics, please refer to the following documentation:
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
- [Billing issue](billing-issue)
---
# File: grace-period-converted
---
---
title: "Grace period converted"
description: "Track the number of subscriptions that entered the grace period and were renewed before it ended."
---
The **Grace period converted** chart displays the number of subscriptions that entered the [grace period](grace-period) state, and were successfully renewed before the period elapsed.
### Calculation
The Grace period converted chart displays the daily number of subscription renewals for users in a grace period.
The Grace period begins when the subscription enters the billing issue state due to a payment failure and ends after a specified amount of time (6 days for weekly subscriptions, 16 days for all other subscriptions) or when payment is successfully received. The chart provides insights into the effectiveness of the grace period feature and can help identify potential issues with payment processing or subscription management.
### Available filters
- ✅ Filter by: Attribution, audience, refund reason, country, offer type, offer ID, offer discount type, A/B test, paywall, store, placement, period, segment, product, and duration.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation](controls-filters-grouping-compare-proceeds).
### Grace period converted chart usage
Use this chart to track how effectively the grace period feature helps recover subscriptions with payment issues. By monitoring conversion trends over time, you can identify patterns in payment resolution and assess the impact of changes to your payment update flows or communication strategies during grace periods.
### Similar metrics
- [Billing issue](billing-issue)
- [Billing issue converted](billing-issue-converted)
- [Billing issue converted revenue](billing-issue-converted-revenue)
- [Grace period](grace-period.md)
- [Grace period converted revenue](grace-period-converted-revenue)
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
---
# File: grace-period-converted-revenue
---
---
title: "Grace period converted revenue"
description: "Track the total revenue for grace period conversions."
---
The **Grace period converted revenue** chart displays the revenue generated from [grace period conversions](grace-period-converted): subscriptions that entered the [grace period](grace-period) state, and were successfully renewed before the period elapsed.
### Calculation
The Grace period converted revenue chart displays the daily revenue generated by subscription renewals of users in a grace period.
The Grace period begins when the subscription enters the billing issue state due to a payment failure and ends after a specified amount of time (6 days for weekly subscriptions, 16 days for all other subscriptions) or when payment is successfully received. The chart provides insights into the effectiveness of the grace period feature and can help identify potential issues with payment processing or subscription management.
### Available filters
- ✅ Filter by: Attribution, audience, refund reason, country, offer type, offer ID, offer discount type, A/B test, paywall, store, placement, period, segment, product, and duration.
You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds)
### Grace period converted revenue chart usage
Use this chart to measure the financial impact of the grace period feature by tracking revenue recovered from subscriptions with payment issues. This helps you quantify the effectiveness of your grace period strategy and assess the return on investment of implementing grace period-related features or communications.
### Similar metrics
- [Billing issue](billing-issue)
- [Billing issue converted](billing-issue-converted)
- [Billing issue converted revenue](billing-issue-converted-revenue)
- [Grace period](grace-period.md)
- [Grace period converted](grace-period-converted)
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
---
# File: billing-issue
---
---
title: "Billing issue"
description: "Resolve subscription billing issues using Adapty’s support tools."
---
The Billing issue chart displays the number of subscriptions that have entered the Billing Issue state. This state is typically triggered when the store, such as Apple or Google, is unable to receive payment from the subscriber for some reason. This could happen due to reasons such as an expired credit card or insufficient funds.
### Calculation
Adapty calculates the billing issue chart by tracking the number of subscriptions that have entered the billing issue state during a given period. A subscription enters the Billing Issue state when the store (e.g. Apple, Google) is unable to process payment from the subscriber for some reason, such as an expired credit card or insufficient funds. During the Billing Issue state, the subscription is not considered active, and if the Grace Period feature is enabled in the store settings, the subscription will only move to the Billing Issue state after the Grace Period has expired.
### Available filters and grouping
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative.
You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds)
### Billing issue chart usage
The Billing Issue chart can provide valuable insights into your app's subscription performance and revenue. By tracking the number of subscriptions in the Billing Issue state over time, you can identify patterns and potential issues related to payment processing and user payment information. This information can be used to optimize your app's payment process and reduce the likelihood of subscription cancellations due to payment issues. You can also use the chart to track the impact of changes to payment processing and billing information update flows.
### Similar metrics
In addition to the Billing Issue chart, Adapty also provides metrics for other issues-related events, such as Refund events, Refund money, and Grace period. To learn more about these issue-related metrics, please refer to the following documentation:
- [Billing issue converted](billing-issue-converted)
- [Billing issue converted revenue](billing-issue-converted-revenue)
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
- [Grace period](grace-period.md)
- [Grace period converted](grace-period-converted)
- [Grace period converted revenue](grace-period-converted-revenue)
---
# File: billing-issue-converted
---
---
title: "Billing issue converted"
description: "Track the number of billing issues that are resolved before the end of the billing cycle."
---
The Billing issue converted chart displays the daily number of subscriptions that entered the [Billing Issue](billing-issue) state, and were renewed before the end of the billing cycle.
### Calculation
The Billing issue converted chart displays the number of subscriptions that entered the [Billing Issue](billing-issue) state in the current billing cycle, and were renewed on the day.
A subscription enters the Billing Issue state when the store (e.g. Apple, Google) is unable to process payment from the subscriber for some reason, such as an expired credit card or insufficient funds. During the Billing Issue state, the subscription is not considered active, and if the Grace Period feature is enabled in the store settings, the subscription will only move to the Billing Issue state after the Grace Period has expired.
### Available filters
- ✅ Filter by: Attribution, audience, refund reason, country, offer type, offer ID, offer discount type, A/B test, paywall, store, placement, period, segment, product, and duration.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation](controls-filters-grouping-compare-proceeds).
### Billing issue converted chart usage
Use this chart to track how effectively billing issues are resolved within the billing cycle after the grace period expires. By monitoring resolution trends over time, you can identify patterns in payment recovery and assess the impact of changes to your payment retry logic or communication strategies during billing issues.
### Similar metrics
- [Billing issue](billing-issue)
- [Billing issue converted revenue](billing-issue-converted-revenue)
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
- [Grace period](grace-period.md)
- [Grace period converted](grace-period-converted)
- [Grace period converted revenue](grace-period-converted-revenue)
---
# File: billing-issue-converted-revenue
---
---
title: "Billing issue converted revenue"
description: "Resolve subscription billing issues using Adapty’s support tools."
---
The **Billing issue converted revenue** chart displays the revenue from [billing issue conversions](billing-issue-converted): subscriptions that entered the [Billing Issue](billing-issue) state, and were renewed before the end of the billing cycle.
### Calculation
The **Billing issue converted revenue** chart displays the daily revenue from subscriptions that entered the [Billing Issue](billing-issue) state in the current billing cycle, and were renewed on the day.
A subscription enters the Billing Issue state when the store (e.g. Apple, Google) is unable to process payment from the subscriber for some reason, such as an expired credit card or insufficient funds. During the Billing Issue state, the subscription is not considered active, and if the Grace Period feature is enabled in the store settings, the subscription will only move to the Billing Issue state after the Grace Period has expired.
### Available filters
- ✅ Filter by: Attribution, audience, refund reason, country, offer type, offer ID, offer discount type, A/B test, paywall, store, placement, period, segment, product, and duration.
You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation](controls-filters-grouping-compare-proceeds).
### Billing issue converted revenue chart usage
Use this chart to measure the financial impact of resolved billing issues by tracking revenue recovered from subscriptions after the grace period expires. This helps you quantify the effectiveness of your billing issue recovery strategy and assess the return on investment of implementing billing retry mechanisms or targeted user communications.
### Similar metrics
- [Billing issue](billing-issue)
- [Billing issue converted](billing-issue-converted)
- [Refund money](refund-money.md)
- [Refund events](refund-events.md)
- [Grace period](grace-period.md)
- [Grace period converted](grace-period-converted)
- [Grace period converted revenue](grace-period-converted-revenue)
---
# File: ltv
---
---
title: "Lifetime Value (LTV)"
description: "Learn how to calculate and optimize Lifetime Value (LTV) in Adapty."
---
The Realized LTV (Lifetime value) per paying customer displays the revenue that a paying customer cohort actually generated after refunds have been deducted, divided by the number of paying customers in that cohort. Therefore, this chart tells you how much revenue you generate on average from each paying customer.
Adapty designs the LTV chart to answer several important questions about your app's revenue and customer behavior such as:
1. How much money does each cohort bring in over their lifetime with your app?
2. At what point in time does a cohort pay off?
3. How can you optimize your app's marketing and acquisition spend to attract valuable, high LTV customers?
4. How long does it take to recoup your investment in acquiring new customers?
The LTV chart works with the app data we gather through our SDK and in-app events.
With this information, you will be able to gain insights into how your subscriptions are performing and how much revenue is generated from your subscribers during a given period of time. You can use this information to make informed decisions about your subscription offerings, ad spending, and customer acquisition strategies. Additionally, the filters will allow you to segment the data by country, attribution, and other variables, giving you a more granular understanding of your customer base.
### LTV by renewals
The **LTV by renewals** view presents data pertaining to the subscription period (P), specifically capturing the first instance when a customer makes a payment. In the case of a weekly subscription, this corresponds to the subsequent weekly subscription period.
### LTV by days
The **LTV by days** view organizes and filters data based on daily, weekly, or monthly intervals. It provides insights into the total revenue generated by all users who installed the app on a specific day, week, or month, divided by the number of paying users during that same period. This view offers valuable insights into revenue tracking and enables a comprehensive understanding of user behavior over time.
### Calculation
Realized LTV is calculated using the total revenue generated from each customer cohort, minus refunds.
_LTV for the day/week/month = Revenue gained from all the paying users who installed the app on this day/week/month / the number of paying users who installed the app on this day/week/month_
The LTV calculation includes upgrades, downgrades, and reactivations, such as when a user changes their subscription plan or pricing. It takes into account the revenue generated from the initial subscription and subsequent renewals based on the updated plan.
### LTV chart usage
The LTV chart is a valuable tool in Adapty that provides insights into the long-term value of your customers. By analyzing customer behavior and purchase patterns over time, the LTV chart helps you understand the revenue generated by different customer segments or cohorts.
The LTV chart is particularly useful for identifying high-value customer segments, tracking the effectiveness of marketing campaigns, and evaluating the overall financial performance of your business. By understanding the lifetime value of your customers, you can make informed decisions about resource allocation, customer acquisition strategies, and customer retention initiatives.
In addition, the LTV chart can be used to compare different customer segments, assess the impact of product changes or pricing adjustments, and identify opportunities for upselling or cross-selling.
### Available grouping and filtering
Both filters and groupings can be applied to both the renewals and days views of the LTV chart, allowing you to drill down into specific cohorts and understand their behavior over time
- ✅ Filter by: Attribution, country, paywall, store, product, and duration.
- ✅ Group by: Product, country, store, duration, and by cohort day, week, month, and year.
You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
The Realized LTV chart in Adapty helps to gain valuable insights into customer behavior, optimize marketing strategies, track revenue performance, and make data-driven decisions to maximize the long-term value of their customers.
---
# File: analytics-cohorts
---
---
title: "Cohort analysis"
description: "Use analytics cohorts in Adapty to track user engagement and subscription trends."
---
Adapty cohorts are designed to answer several important questions:
1. On what day does a cohort pay off?
2. How much money does the app earn for a specific cohort?
3. How much money can I spend to attract a paying customer?
4. How long does it take to recoup the ad spend?
Cohorts work with the app data we gather through SDK and store notifications and don't require any additional configuration from your side.
### Cohorts by renewals or by days
You can analyze cohorts by renewals or by days. The control changes the headings of the columns. Consequently, the approach to analysis also changes.
Tracking **by days** provides valuable insights for budgeting and understanding payment timelines. This is particularly useful for tracking non-subscription products, such as consumables or one-time purchases. In this mode, the blue color in the table cells tends to be concentrated in the middle of the lines due to two key factors. Firstly, viewing cohorts by days allows for early visibility of payments associated with short duration products, while in the renewals view, they are grouped with monthly and yearly renewals. Secondly, delayed payments contribute to the distribution pattern, as some users pay later than expected.
Whereas tracking **by renewals** shows the retention and churn of the cohorts from one payment to another without consideration of the date. So late users who paid with any delay (it can be months) are added to the number of their subscription period. This approach doesn't reflect the situation of calendar earnings but is definitely more convenient to analyze the retention and churn of the cohorts and get insights from their behavior.
Choose your convenient mode or use them both for more conclusions and ideas.
### How Adapty builds cohorts
Let's see in the example of cohorts by renewals how the table is formed. To build cohorts, we use two measures: app installations and transactions (purchases). Every row of a cohort represents a specific time interval: from a day to a year. Each row starts with the number of users who installed the app during this interval and activated a subscription or made a lifetime/non-subscription product purchase.
Every next column in the row shows the number of users who renewed a subscription to this period. M3 stands for month 3 and means that subscribers had 3 consecutive renewals to this point, W7 stands for week 7, and Y2 stands for year 2. Sometimes you can see P2 in cohorts. P stands for Period of subscription. Adapty displays instead of W/M/Y when there are multiple products with different renewal periods present in the same cohort.
We use gradient colors to highlight differences in cohort values. The biggest numbers have more saturated colors.
In the image below you can see a typical cohort.
1. This cohort displays the data only for weekly products (mark #1).
2. It doesn't exclude proceeds and shows the revenue as absolute values (mark #2).
3. The time period we're working with is the last 6 months, and every cohort segment is 1 month long (mark #3).
4. The **Total** row (mark #4) displays the cumulative value for each period. $442K in the first cell of the **Total** row accumulates the first period (subscription activation) revenue from all months (Nov, Dec, and so on) until the end of the timeframe. The Total cell shows the number of customers who installed the app during the whole period.
5. The first column of the Nov 2023 row (mark #5) shows the first period (subscription activation) revenue of $37.7K from the customers who installed the app in Nov 2023. The number of customers who installed the app in Nov 2023 which is 95,129, is shown in the header column.
The second column of the Nov 2023 row shows week 2 (subscriptions renewed to the 2nd week) revenue of $8,77K who installed the app in Nov 2023.
6. On the table, you can see the Total revenue, ARPU, ARPPU, and ARPAS (mark #6). You can read more about them a little further in this article.
7. You can configure the columns in the right part of the table using the **Columns** dropdown field (mark #7).
8. Above the table on the right (mark #8), there is also a dropdown field to calculate stores' commission fees and tax calculations for the specific cohort analyses. You can learn about how Adapty calculates store commission fees and taxes further in [this article](controls-filters-grouping-compare-proceeds#store-commission-and-taxes). After choosing the corresponding option from the dropdown the revenue data will be recalculated based on it.
9. On the right side of the table, you can see predicted revenue (Predicted Revenue) and predicted lifetime value (Predicted LTV) (mark #9). The **Predicted Revenue** field estimates the total revenue generated by a subscriber cohort within a specific timeframe, while the **Predicted LTV** field represents the anticipated value of each user in the cohort.
You can hover on any cell in the cohort to view detailed metrics for this period.
The cells with oblique lines in the background are the periods that are not finished yet, so the values in them might be increased.
### Filters, metrics, cohort segments, and export in CSV
Adapty offers a wide range of controls to help you gain valuable insights when looking into your cohorts' analyses. By default, Adapty builds cohorts based on the data from all purchases. It might be useful to filter all the products of the same duration or specific products. You can also use country, store, paywall, segment, and attribution data as a filter. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds)
On the right of the control panel, there's a button to export cohort data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system.
There are 4 metrics that can be shown in cohorts: Subscriptions, Payers, Revenue, ARPU, ARPPU, and ARPAS. You can either display them as absolute values or as a relative change from the start of the cohort.
You can set the date range for cohorts and choose the segment. The segment determines a timespan for each row of the cohort.
### Subscriptions, payers, total revenue, ARPU, ARPPU and ARPAS
**Subscriptions** are the total count of active subscriptions, lifetime purchases, and non-subscription purchases made by a cohort within a selected timeframe. Monitoring this metric helps you understand customer behavior and the effectiveness of your offerings. This insight allows you to refine your product strategy, tailor marketing efforts, and optimize revenue streams.
**Payers** are the total number of users who made a purchase within a cohort. It helps you understand how many unique users contribute to your revenue. For apps with a significant amount of non-subscription purchases, this metric can highlight the true reach of your product offerings, showing whether a broad user base is making purchases or if revenue is driven by a smaller group of repeat buyers. Understanding the number of payers helps in assessing customer engagement, planning targeted marketing, and optimizing revenue strategies.
**Total revenue** is accumulated for a cohort within a selected timeframe (Nov 25, 2022 — May 24, 2023). It helps you to understand how much money you collected from users from a specific cohort and calculate ROAS. For example, if the ad spend for September 2022 was $10000, and the total proceeds for September 2022 cohort are $30000, ROAS=3:1.
**ARPU** is the average revenue per user. It’s calculated as total revenue / number of unique users. $60000 revenue / 5000 users = $12 ARPU. It’s helpful to compare this value to the cost of install (CPI) to understand the effectiveness of your marketing campaigns.
**ARPPU** is the average revenue per paying user. It’s calculated as total revenue / number of unique paying users. $60000 revenue / 1000 paying users = $60 ARPPU. It helps you to understand how much money brings you a paying customer on average.
**ARPAS** is the average revenue per active subscriber. It’s calculated as total revenue / number of active subscribers. By subscribers, we mean those who activated a trial period or subscription. $60000 revenue / 1500 subscribers = $40 ARPAS.
### Commission fees and taxes
One important aspect of revenue calculation in cohorts is the inclusion of store commission fees and taxes (which can vary based on the user's store account country) and store commission fees. Adapty currently supports commission fee and taxes calculation for both App Store and Play Store in cohort analytics.
For more details on how Adapty calculates taxes and commissions in its analytics, please refer to our [documentation](controls-filters-grouping-compare-proceeds#store-commission-and-taxes).
## Revenue vs Proceeds
Both Revenue and Proceeds are money metrics. You can think of Revenue as gross revenue and Proceeds as net revenue. Revenue doesn't account for App Store / Play Store fees, while Proceeds do. Therefore Proceeds are always less than Revenue.
The actual commission deducted varies based on multiple factors, including eligibility for programs like the [Small Business Program](app-store-small-business-program) (15%), reduced rates for long-term subscriptions (15% after one year of renewal), country-specific rates (such as 26% in Japan), and standard rates (up to 30%).
Adapty automatically determines the applicable commission rate for every transaction your customers make and calculates Proceeds based on it. For more information on how commission rates are determined, see the [Store commission and taxes](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) documentation.
### Prediction: Revenue and LTV
**Predicted revenue** is an estimated total revenue a cohort of paying subscribers is expected to generate within the selected period after cohort creation. It is calculated by multiplying the predicted LTV of the cohort by the predicted number of paying users within the cohort. For example, if the predicted LTV is $50 and there are 100 paying users in a cohort, the Predicted Revenue would be $5,000.
**Predicted LTV** is the estimated lifetime value per paying subscriber, representing the average revenue each paying subscriber is expected to generate within the selected period after cohort creation.
These predictions are done using machine learning (ML) models, which analyze historical customer data to identify patterns and make predictions about future revenue. For detailed documentation on Adapty's prediction models, please refer to our [Prediction documentation](predicted-ltv-and-revenue).
Adapty cohorts provide detailed insights into user behavior and financial performance within your app. By analyzing cohorts based on renewals or days, you can determine when cohorts become profitable, track revenue, calculate average revenue per user, and understand the time it takes to recoup advertising spend. With customizable filters, metrics, and export options, Adapty empowers you to make data-driven decisions and optimize user acquisition and monetization strategies for maximum app success.
---
# File: analytics-funnels
---
---
title: "Funnel analysis"
description: "Understand analytics funnels in Adapty to monitor user behavior and improve conversions."
---
Adapty funnels are designed to assist you with such kinds of questions:
1. What percentage of installs is converted to paying clients?
2. What part of those who tried the product became loyal?
3. Which steps show high drop-off and need more attention?
4. Why do clients stop to pay?
With a funnel chart, you may also find more insights about user behavior setting filters and groups.
Funnels work with the data that we gather through SDK and store notifications and don't require any additional configuration from your side.
:::note
Funnels reflect install data according to your install definition in [App Settings](general.md#4-installs-definition-for-analytics).
:::
## Funnel chart step by step
Let's go through the elements of a funnel to understand how to read the user journey on the chart.
### Installs
The 1st column (1) is the number of installs. It is shown as an absolute value (2) of total installations (not unique users) and also as 100% - the largest input number for further conversions relative calculation. If a user deletes an app and then installs it again two separate installs will be counted.
A grey area nearby stands for transition parameters between steps. A conversion percent to the next step (Displayed paywall) is shown on a flag (3). Drop off percent and an absolute value of churn are shown below (4).
### Paywall displayed
The 2nd column (5) shows the number of users of the app who saw a paywall at least one time (6). They are taken only from those installs that happened in a selected period. If a user sees a paywall in the selected period but his install date is out of range his view is not counted.
There is also a percentage of such views taken from the 1st step (7). You may notice that this percent is equal to the grey flag (3) of the 1st step. This equality takes place only for these first steps.
We collect data for this step from all your paywalls that use the `logShowPaywall()` method. So please be sure to send every paywall view to Adapty using this method as described in the [docs](present-remote-config-paywalls#track-paywall-view-events).
A grey area next to the 2nd column stands for transition. A conversion percent to the next step (Trial) is shown on a flag (8). Drop-off percent and the absolute value of churned customers after the paywall are shown below (9).
### Trials
The 3rd column (10) shows the number of trials activated on the paywalls by customers who installed the app within a selected period (11). If a filter is set to non-trial product(s) this value becomes zero and the column is empty.
See also a percent of trials taken from the 1st step, showing the conversion from installs to trials (12).
You may notice that this percent is not equal now to the grey flag (8) of the previous step conversion. This is because we compare the current value with the 1st step at the top of the chart and with the previous step on grey flags.
So a grey area next to the 3rd column shows a conversion percent to the next step (Paid) which is displayed on a flag (13). Drop-off percent and absolute value of churned customers during a trial period are shown below (14).
### Subscriptions and renewals
The 4th column shows the number of activated subscriptions (15). For products without trials, this number includes direct subscriptions from a paywall. For products with trials, it contains the number of trials converted into paid subscriptions. If you have both types of products, with trial and without, it will be a sum of both.
The percent at the top shows the conversion from installs (16).
The percent on a grey flag shows conversion to the next step (renewal to the 2nd period) (17).
Drop off before the renewal to the 2nd period percent and absolute value are shown below the conversion (18).
This step starts a sequence of steps with a similar structure. After the 2nd renewal comes the 3rd, then the 4th, etc. If there is enough data in your app history you may see dozens of periods using the horizontal scroll. The logic for these steps remains the same:
- percent from installs at the top,
- percent from the previous step at the bottom,
- the absolute amount of renewal at the top,
- the absolute amount of churn at the bottom,
- a hover for churn reasons pop-up.
### Churn reasons
Adapty details *churn* statistics for the Trial stage and later. Every user who entered one stage, but not the next, counts as an instance of churn.
* If a specific event (for example, a trial expiration or a billing issue) caused the lack of conversion, Adapty displays the reason.
* The **unknown** status is a temporary state. It indicates that the user hasn't yet encountered the event that allows them to proceed to the next stage.
In the Trial stage, this usually means the trial has not yet ended. This commonly occurs when viewing Funnels for short date ranges or single days, since trials take time to resolve.
Adapty will update the information once the user converts or cancels the trial.
### Table view, filters and CSV export
A funnel chart is enriched with data in a table to provide handy material for your work with numbers.
This table repeats the approach of the funnel with some amendments.
There are columns that show data on all steps except for the step of the 1st paid subscription.
Instead of this one, there are two separate: Install -> Paid and Trial -> Paid. They display a core point of conversion when a free user becomes paying.
It may seem that there is a product type division: Install -> Paid column shows only products without trials while the column Trial -> Paid contains only products with trials. But that's not exactly the way it works. Because we also consider those users whose trial has expired and they purchase a product with a trial like it doesn't have it at all.
Diving deeper into numbers you will find filtering powerful tools for new hypotheses.
Feel free to set conditions in different dimensions. Collect true insights based on data.
Variate:
1. Product type - economy, length, etc
2. Time range.
3. Country segmentation.
4. Traffic attribution.
5. Store.
Select Absolute #, Relative %, or both to view only necessary data.
Finally, on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system.
:::important
Notify Adapty if your app is enrolled in a reduced commission program. To ensure correct calculations, specify your [Small Business Program](app-store-small-business-program) and [Reduced Service Fee program](google-reduced-service-fee) status in your [app settings](general).
:::
---
# File: analytics-retention
---
---
title: "Retention analysis"
description: "Understand user retention analytics and optimize your subscription strategy."
---
Retention charts can help with the following questions:
1. How does your app retain clients from period to period?
2. What products are more attractive and hold better?
3. What groups of users are more loyal?
4. Which level of retention can be used as a benchmark for growth?
5. And of course, how can you save money investing in the attracted audience instead of capturing new.
You'll find valuable insights about user behavior setting filters and groups.
Retention is performed with the data that we gather through SDK and store notifications and don't require any additional configuration from your side.
### How do we calculate retention?
Observing the retention chart, you see how the number of users depends on the step they take: trial (if the checkbox "show trials" is checked), the 1st payment, the 2nd payment, etc. Let's specify what users are counted when you choose a date range for the retention chart.
For example, you've selected the last 3 months in the calendar, and the checkbox "show trials" is unchecked. This means we count only those who have had their 1st subscription during the last 3 months. If the checkbox "show trials" is checked and the 3 last months are selected in the calendar we count all those who have had their trials during the last 3 months. For these subscribers, we show the absolute retention for the Nth step as the number of those who have had the Nth payment. And we calculate a relative value of retention for the Nth step as a ratio of the absolute amount of the Nth payment to the total amount of subscriptions (or trials) during the selected time range.
:::info
Retention changes retrospectively
Regardless of when you check the chart, the baseline number (100%) remains the same for the selected period of time. Meanwhile, the retention to the next period may grow over time.
For example, for a Monthly subscription, if there are 20 first purchases made between Dec 1 and Dec 31, it is expected that retention to the second period will grow throughout January (and possibly even after) while users will be entering the next subscription period in time or later for some reasons (e.g. grace period).
:::
### Retention opportunities
Let's see how to get more from the Adapty retention feature.
Having not only a pure passion for numbers but more willingly seeing real business value after implementing analytical results, we may think about the purposes first. With a deep dive into chart features, it would be nice to clear up the impact this data can have.
So let's keep in a glance together WHY and HOW.
1 - work with the audience.
First of all, retention is about the target audience, its preferences, and whether your product meets their expectations or not during the consuming lifetime. If you have wondered how to measure the core relationship of your business that generates money - retention is at your service.
Such a measurement benefits because it's usually cheaper to sell to your customer than to a stranger. And this cost is low for two reasons: less effort to sell and higher average check. So it might be a good idea to invest in your subscribers' loyalty when retention goes down.
2 - work with the product.
The second reason WHY is that retention charts show the actual consuming lifetime of your product and let you forecast in long term. And if you want to improve, correct the job that delivers the product to change its lifetime, and then forecast again to become closer to your business targets. Such updates may be a part of a strategic vision working together with a forecasting routine. And yes, this process never ends because we all run fast to be at the same place in a constantly changing environment.
3 - work with the market.
Moving faster than the main competitors is good but sometimes jumping out of the ordinary race may bring more benefits. When you analyze the behavior of users in different countries and stores, some local peculiarities can open outstanding insights and new opportunities for the business. Cultural and market context can be analyzed from the perspective of retention to be later used for segmentation and further development. For example, you may find blue water in some regions and grow there faster.
The usage of retention data is of course, not limited to this basic interpretation but it may be a good start if you want to get real value fast.
### Curves, table view, filters and CSV export
Now when we are on the same page with retention purposes and basic ways of interpretation let's go through the tools that make it all handy.
The core of the retention feature in Adapty is the chart. It shows how retention level depends on the steps of a customer's lifetime.
The steps are shown on the horizontal axis: Trial, Paid (the 1st subscription), P2 (the 2nd subscription), P3, P4, etc
Please mind that the axis starts with the Trial step only when the checkbox "Show trials" is selected.
For data calculation, this checkbox works as follows. When "Show trials" is selected and the axis starts with the Trial step, you see only scenarios that contain trials, no transactions directly from installs are shown and the step Paid contains only transactions that come from trials. When "Show trials" is not selected, and the axis starts with a Paid step, this first step contains all first transactions including both from trials and directly from installs.
When you hover over the chart, a pop-up with a data summary is displayed. And if you hover over a column in the table below, you also see a summary pop-up with relevant data on the chart.
The table contains the same grouping and filters chosen for the chart.
Feel free to combine filters and grouping for advanced analysis. Collect true insights based on data.
Variate:
1. Product type.
2. Duration.
3. Time range.
4. Country.
5. Traffic attribution.
6. Store.
Use #Absolute and %Relative control to view the necessary data.
Finally, on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system to continue analysis and forecasting in your preferred environment.
:::warning
Be sure to indicate that your app is included in Small Business Program in [Adapty General Settings](https://app.adapty.io/settings/general).
:::
---
# File: analytics-conversion
---
---
title: "Conversion analysis"
description: "Measure subscription conversion rates using Adapty’s analytics tools."
---
While funnels give you a high-level overview and retention focuses on user loyalty, conversion analysis is designed to help you evaluate effectiveness at every critical step in the user journey—over time.
Conversions assist with the following questions:
1. How do app conversions change over time? Are there any seasonal trends?
2. How conversions are changed in the moment of marketing activities or some other new circumstances?
3. How do users in different regions respond to your app updates?
4. Which product types convert better over time?
Conversion is performed with the data we gather through Adapty SDK and store notifications, and it doesn't require any additional configuration from your side.
## Main controls and charts
Though revenue is often the go-to metric for measuring success, it's just one part of the bigger picture. Understanding how your business performs over time—across different user behaviors and lifecycle stages—is equally important. That’s where conversion analytics come into play.
You can find more valuable insights about user behavior by setting filters and groups. To identify and analyze trends, monitor how your conversions evolve daily, monthly, or yearly.
On the left side of the chart, you'll find the conversion steps control. This lets you choose which specific conversions to track—such as Install → Trial, Trial → Paid, or Paid → Renewal.
Each conversion metric follows this logic:
- Let **X** be the number of users who entered the starting state on a selected date (e.g., installs).
- Let **Y** be the number of those users who eventually reached the target state (e.g., trial starts).
- The conversion rate is calculated as: **Conversion = (Y / X) × 100%**
:::note
The date shown on the chart corresponds to when users entered the initial state (X)—the moment they became eligible to convert.
:::
Please see below for each conversion explanation, along with an example for your reference.
### Install -> Paid
This metric shows what percentage of users who installed the app on a specific date eventually purchased their first subscription.
If a chosen date range is not enough to show any results, you may see a notification that offers a relevant date and an option to adjust the date range automatically so you may do it with one click.
## Table view, filters and CSV export
A comparison of the curves gives a bright picture, and to get more use the table view below the chart. The table is synchronized with the chart so hovering over a column you see the associated pop-up over the curves.
The grouping that was mentioned above changes both the charts and the table. Set quick filter by product or use other advanced ones, including Product, Country, Store, Duration, Attribution.
We know that it's important to have an option to work with numbers the way you like. So on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system to continue analysis and forecasting in your preferred environment.
:::important
Notify Adapty if your app is enrolled in a reduced commission program. To ensure correct calculations, specify your [Small Business Program](app-store-small-business-program) and [Reduced Service Fee program](google-reduced-service-fee) status in your [app settings](general).
:::
---
# File: reports
---
---
title: "Reports"
description: "Generate detailed subscription reports in Adapty to analyze app revenue and user behavior."
---
Receive timely and relevant information straight to your inbox, including revenue, churn rate, active subscribers, active trials, and more – the same metrics available in [Charts](charts). These reports can arrive on a daily, weekly or monthly basis and show dynamics comparing the most recent period to the one that came before it.
The data we send in the reports is based on what you have configured your [**Overview**](https://app.adapty.io/overview) page meaning metrics, their order, reporting timezone and revenue type.
You have the flexibility to choose the level of detail you prefer for your reports: summary or per-app. A summary report is a single email containing aggregated information on all of your apps (or the subset of them that you have selected). A per-app report, in contrast, will only have the data for a single chosen app. We recommend enabling summary reports for all apps and per-app reports for recently released or high-priority apps, as well as those you are personally responsible for.
Regardless of the level of detail chosen, email reports are delivered to your inbox at 9 AM in your local time zone: daily reports arrive each day, weekly reports arrive on Mondays, and monthly reports arrive on the first day of the month. Each report includes current data along with comparisons to the previous period (e.g., for today's daily report, it compares data from yesterday and the day before; for today's weekly report, it compares data from last week and the week before, etc.).
Rest assured, whichever reports you select, you'll receive the most up-to-date and accurate information directly in your inbox.
## Enable reports
1. Open the [**Account**](https://app.adapty.io/account) section in the Adapty top menu.
2. Under the **Email reports** section, choose the types of reports you wish to receive – daily, weekly, and/or monthly.
2. Customize each report type by selecting the relevant apps. For this, click the **Edit** button.
3. In the report window, choose the apps you want to include.
4. Finally, click the **Save changes** button to apply your selections.
## Set your time zone
1. Open the [**Overview**](https://app.adapty.io/overview) section in the Adapty main menu.
2. Click the **Edit** button and choose your time zone.
3. Click the **Done** button to save.
---
# File: discrepancies-and-troubleshooting
---
---
title: "Troubleshoot data discrepancies"
description: "Find the cause of divergences in data"
---
Adapty users may encounter **discrepancies** when comparing similar sets of data from different sources. In particular, this can occur when you compare:
* Adapty charts to store reports
* Adapty charts to third-party charts
* Different charts within Adapty
## Troubleshooting algorithm
Most discrepancies between Adapty and other platforms are expected and normal. They occur because **different sources process the same data differently**.
Other times, they indicate an **issue with your Adapty configuration**.
If you suspect that your data varies from platform to platform, the best course of action is to [export raw data](export-analytics-api-requests) and **compare the files**.
* Even stores can experience issues related to data processing and presentation. Access the stores' **raw transaction data** for the most accurate comparison.
* When comparing Adapty to another analytics platform, use store transaction reports as the source of truth and point of comparison.
* It's easier to identify inconsistencies with a limited data set. Compare small volumes of data — focus on a specific product and a single day.
* Identify whether your discrepancy stems from a difference in **pricing** or **event number**. Pricing issues can be fixed with a [product update](#product-pricing). Event issues may indicate [server-side problems](#issues-with-server-notifications-and-rtdn).
* View the [event feed](event-feed) to monitor incoming events — you may notice unexpected behavior.
After you identify where the data diverges, you can look into the following common causes:
## Issues with server notifications and RTDN
Adapty does not receive the necessary event data if you didn't correctly configure the store connections. This particularly affects events that occur without direct user involvement — subscription renewals, billing issues, etc.
Complete the server-to-server configuration as soon as you can ([App Store](enable-app-store-server-notifications) | [Play Store](enable-real-time-developer-notifications-rtdn)) and [wait](#data-delays) for the stores to establish the connection.
You can [manually upload](importing-historical-data-to-adapty) the missing App Store Connect data to Adapty.
## Missing data
### Users with out-of-date app versions
If some of your users run an older version of your app without the Adapty SDK, Adapty does not receive their data. For this reason the numbers of Adapty and other sources will diverge.
### Integration issues
Some Adapty integrations (for example, Adjust or AppsFlyer) require additional application code to work. If you configure the Adapty dashboard, but do not update your application, the necessary data won't show up in Adapty.
### Missing historical data
Adapty doesn't have access to your application's historical data, unless you [manually import](importing-historical-data-to-adapty) it. If a chart's [time range](controls-filters-grouping-compare-proceeds#time-ranges) starts before you integrated Adapty, and you didn't import historical data, its values will differ from other sources.
## Data delays
Adapty aims to provide near real-time analysis of your application's economy. The following limitations and exceptions apply:
* When you first integrate Adapty, the data may not appear immediately.
* When you enable an integration with a third-party platform, there may be a delay before the data is fully synchronized.
* Once Adapty receives store data, it takes another **15-30 minutes** for it to be processed and displayed on the Analytics page.
* Data exchange between Adapty and third parties is **not always instantaneous** due to the number of variables involved.
* Calculations for some advanced metrics (such as [cohort predictions](predicted-ltv-and-revenue)) require a certain amount of data. Adapty will only perform these calculations when it gathers enough data.
## Time and calendar
#### Dates and timezones
One of the most common reasons for perceived data discrepancies is a difference in timezone settings.
Adapty counts days according to the `UTC` timezone. If another platform uses a different timezone, the calculations will differ. The difference will decrease as you increase the scale.
You can [change the timezone setting](general#3-reporting-timezone) for each application.
#### The Apple fiscal calendar
Apple uses its own [accounting calendar](https://adapty.io/apple-fiscal-calendar/) to determine sales periods and payout dates.
Each "month" in the calendar consists of **4 or 5 weeks**, and **may include days from the neighboring calendar months**. Payments are typically issued 30–45 days after the sales period ends.
For example, the "January 2026" sales period begins on December 28th, 2025 — 4 days before the start of the calendar month. The estimated payment date for this period is March 5th.
Do not compare data from Apple payout reports to calendar months. Instead, select a [custom date range](controls-filters-grouping-compare-proceeds#time-ranges) that corresponds to the necessary sales period.
#### Transaction dates
Some services (for example, AppsFlyer) may apply [cohort](analytics-cohorts) rules when displaying transactions, and attribute them to the application's install date, rather than the date the transaction itself occurred.
## Revenue calculation
### Fees and taxes
Depending on the [setting](controls-filters-grouping-compare-proceeds#store-commission-and-taxes), Adapty charts can display your **gross revenue**, **revenue after store commission**, or **revenue after store commission and tax**.
Some stores and third-party platforms may lack the capability to display gross revenue, or automatically deduct taxes. If you see a discrepancy between two different revenue charts, make sure that the comparison is valid.
### Cancellations and refunds
Different platforms display refund data differently. Adapty treats refunds as negative revenue. If a user subscribes, and requests a refund the next day, both events will be reflected in Adapty charts — each on its own day. Other platforms may subtract the refund value from the original transaction.
## Sandbox purchases
The [event feed](event-feed) displays purchases made by sandbox accounts. The analytics charts do not. However, if your historical import data contains sandbox purchases, Adapty won't be able to tell them apart, and its charts will reflect historical sandbox purchases.
## Installs and downloads
Stores (Apple App Store in particular) can track user downloads directly. Their statistics may include cases where the application was installed, but never launched.
Adapty can only register an installation when a user launches the application, regardless of your [install definition](general#4-installs-definition-for-analytics).
## Country and store
To ensure accurate reporting, Adapty [may infer](controls-filters-grouping-compare-proceeds#filtering-and-grouping) the user's country from their IP. Stores always attribute downloads and purchases to a specific app store.
If you need to clearly distinguish between the two, you can [create a new user segment](segments) with the `Country by store account` attribute, and [filter analytics by segment](controls-filters-grouping-compare-proceeds#filtering-and-grouping).
## Product pricing
If incorrect product pricing causes a revenue discrepancy, changing the price doesn't fix it retroactively. To change existing transactions' prices, you need to forcibly override them by importing correct data.
When a user restores an old purchase after a price change, Apple may incorrectly report the purchase's value. You need to import historical data for Adapty to reflect the correct value.
## Attribution conflicts
Adapty can only use [a single attribution source](attribution-integration#prevent-data-issues) for each transaction. You cannot override this data later on.
If your setup includes multiple attribution providers that disagree with one another, the same transaction on two different platforms may appear to have two different traffic sources.
## Differences in terminology
Different platforms may have different names for the same concept. Metrics related to [revenue](#fees-and-taxes) vary in name from platform to platform:
| Adapty | App Store Connect | Google Play Console |
|--------|-------------------|----------------------|
| **Gross revenue** | Sales | Gross Revenue |
| **Proceeds after store commission** | N/A | N/A |
| **Proceeds after store commission and taxes** | Proceeds | Earnings |
| **ARPPU** | Proceeds per paying user | ARPPU |
Other metrics may differ in definition, as well:
- **Subscriptions**:
- Adapty does not count new trials as subscriptions. A [new subscription](reactivated-subscriptions) always starts with a financial transaction.
- Other platforms, such as Google Play Console, may count **each trial as a new subscription**, even before the first payment was made.
- **Retention**:
- Adapty measures retention based on the number of subscription renewals.
- App Store Connect considers a user retained if they open the application on the specified day. A user without a subscription will count, but the subscribed user who didn't open the app on that day won't.
- Google Play Console's "Retained Installers" metric measures retention based on the number of days the application remains installed on the user's device. Users that don't open the application count towards this metric.
---
# File: predicted-ltv-and-revenue
---
---
title: "Predictions in cohorts"
description: "Use Adapty’s predictive analytics to forecast LTV and revenue."
---
Adapty Predictions are designed to help you answer the following questions:
1. What is the predicted lifetime value (LTV) of your user cohorts?
2. Which cohorts are likely to generate the highest revenue in the future?
3. How much you can invest being aware of the predicted payoff?
With Adapty prediction, you can gain valuable insights and make data-driven decisions to optimize your revenue and growth strategy.
Adapty's prediction model is a powerful new feature that uses machine learning to help you to get a better understanding of the long-term revenue potential and behavior of your app's users. Using advanced gradient boosting techniques, the LTV prediction model can estimate the total revenue a user is expected to generate during the lifetime as a subscriber. This will help you to make informed decisions about user acquisition, marketing strategies, and product development.
Currently, the LTV prediction model provides two types of predictions: predicted LTV and predicted revenue.
### Predictions in cohorts
Adapty now offers the ability to predict the lifetime value (LTV) of users and their predicted revenue for subscription-based apps. These predictions can be displayed on the cohort analysis page for 3, 6, and 12 months.
However, it is important to note that the model does not currently work for lifetimes and for one-time purchases. Additionally, the accuracy of the LTV model may be lower for very new apps with limited data and for apps that have experienced significant changes in their user traffic.
#### Prediction model
The prediction model is built using gradient boosting, a highly accurate and efficient machine-learning algorithm that can handle large datasets. By leveraging transaction data from your subscription-based apps, our model can predict the LTV for users a year after their profile was created. The data used for the model is completely anonymized.
The model is trained on data from all of your apps within Adapty. However, the predicted values are further refined and tailored based on the specific behavior patterns observed in cohorts associated with each individual app. This post-correction algorithm ensures more accurate predictions, taking into account the unique characteristics of each app.
The model achieves a high level of accuracy, with a Mean Absolute Percentage Error (MAPE) of slightly below 10%. This level of precision allows businesses to confidently rely on the model's predictions when making data-driven decisions.
Adapty utilizes two distinct gradient-boosting models to forecast LTV:
- **Revenue prediction model:** Predicts the total revenue generated by a cohort of users.
- **LTV prediction model:** Predicts the average LTV of users in a cohort.
The trained models are then used to predict the total revenue and average LTV of each cohort within the selected period (3, 6, 9, or 12 months) after cohort creation. These predictions are verified and constrained to ensure they are consistent with typical user behavior patterns.
The predictions are initially generated after 1-3 weeks following the creation of the cohort. This timeframe allows for sufficient data gathering and analysis. Suppose a cohort is created on January 1st. Predictions for that cohort would be available sometime between January 15th and January 29th.
After the initial prediction generation, the predictions are then updated daily using the latest transactional data available for the cohort. This frequent updating ensures that the predictions remain current and reflect the most recent behavior of the cohort.
The model takes into account a variety of significant statistics pertaining to the cohorts, including revenue generated from past subscription periods, user retention rates, present LTV, subscription type, the proportion of users from Google Play and App Store, and geographic distribution of users by country, among others. These features are meticulously chosen to guarantee that the model captures the most pertinent information required to generate precise forecasts about the future LTV of users.
The model typically reflects changes in product performance, such as increased retention for monthly subscriptions, with a delay of approximately one week.
The ML model used to predict revenue and LTV has certain limitations that should be taken into account when interpreting its results. These limitations include:
- Data quality: The model's performance depends on the quality and representativeness of the data available. Data quality is a crucial aspect of data analysis, and one of the main reasons for insufficient results is when a cohort behaves unusually and deviates from normal cohort metrics for a particular app or all other apps. This type of data is considered uncommon for the model, which can lead to unexpected results. This situation is more common for new, unusual products or new apps that haven't been included in the training set.
- Time frame: The model can predict values up to 12 months from the creation of a user cohort. You will see growing actual revenue but the prediction will remain at the point of 12 months.
- Subscription durations: The model shows the best performance on monthly and weekly subscriptions
#### Prediction in Adapty cohorts
To access predicted revenue and predicted LTV values for your subscribers, you can navigate to the Cohort Analyses page in your Adapty dashboard. Also, if you want to learn more about the Adapty cohort, please reference our [documentation](analytics-cohorts) about it.
**The predicted revenue (pRevenue)** column shows the estimated total revenue a cohort of subscribers is expected to generate during the selected time frame after cohort creation. This value is calculated using Adapty's revenue prediction model, which utilizes advanced gradient boosting techniques to predict revenue for users.
**The predicted LTV (pLTV)** column shows the estimated lifetime value of each user in the selected cohort. This value is calculated by dividing the predicted revenue by the predicted number of paying users in the cohort. The predicted number of paying users is calculated using Adapty's base prediction model, which predicts the number of paying users in a cohort.
To define the time period for which the predicted revenue and predicted LTV values are displayed, you can select the desired value from the timeframe dropdown in the user interface. The available options are typically 3, 6, 9, or 12 months after cohort creation.
To provide even more valuable insights, Adapty allows you to filter predicted revenue and LTV by product. By default, Adapty builds predictions based on all purchase data, but filtering by product can help you better understand how each product is performing and how it contributes to your overall predicted revenue and LTV.
When there is insufficient data available to generate accurate predictions, the corresponding fields will be greyed out in the user interface. Hovering over the greyed-out fields will display a tooltip with the message "**Insufficient data for accurate prediction**". This serves as a visual cue that the predicted values may not be reliable and further data collection and analysis may be necessary to generate accurate predictions. The lack of data may occur due to several reasons, such as insufficient time since cohort creation, a small cohort size, or an unpopular subscription type. In some cases, the cohort may behave unusually, deviating from the normal metrics used to train the model. Waiting a few weeks may help resolve this issue.
Also, another possible case is there are no values for the prediction. Empty results may occur if there is either insufficient data to make any predictions (usually at least 1-3 weeks of data is needed), or if the maximum time frame for prediction has already passed, i.e., it has been more than a year since the cohort was created.
:::warning
When enabling predictions, it's important to note that there may be a maximum delay of 24 hours before the prediction data for Revenue and LTV becomes available on your Adapty dashboard.
:::
Adapty cohorts provides valuable insights into your revenue and user LTV (lifetime value) by showing current revenue and current money per paying user. By tracking these metrics, you can monitor how your actual numbers are progressing toward the predicted values and track your progress over time. While the primary purpose of using prediction numbers in building plans, it's important to exercise caution and not solely rely on these predictions for decision-making. Rather, they should be used as a guide to inform your strategy and help you make informed business decisions. By leveraging Adapty cohorts and predict, you can gain a better understanding of your revenue and user behavior, and use this information to optimize your business operations for greater success.
---
# File: predictions-in-ab-tests
---
---
title: "Predictions in A/B tests"
description: "Learn how predictions in A/B tests help refine subscription pricing strategies."
---
Welcome to the Adapty Predictive Analytics documentation for our A/B testing feature. This tool will provide insights into the future results of your running A/B Tests and help you make data-driven decisions faster 🚀 with Adapty's ML-powered predictions.
### What are A/B test predictions?
Adapty's A/B Test Predictions employ advanced machine learning techniques (specifically gradient boosting models) to forecast the long-term revenue potential of the paywalls that are compared in an A/B test.
This predictive model enables you to select the most effective paywall based on projected revenue after a year, instead of relying only on the metrics you observe while the test is running. This allows you decide on the winner more reliably and faster, without having to wait weeks for the data to accumulate.
### How does the model work?
The model is trained on extensive historical A/B test data from a variety of apps in different categories. It incorporates a wide range of features to predict the revenue a paywall is likely to generate in a year after the experiment start. These features include:
- User transactions and conversion rates over different periods
- Geographic distribution of users
- Platform usage (iOS or Android)
- Opt-out and refund rates
- Subscription products and their period lengths (daily, monthly, yearly and so on)
- Other transaction-related data
The model also accounts for trial periods in paywalls, using historical conversion rates to predict revenue as if users were already converted. This ensures a fair comparison between paywalls with and without trial offers, because we will also account for active trials potentially bringing in revenue in the future.
### How is Predicted P2BB different from just the P2BB?
Our A/B tests utilise the Bayesian approach: basically we model the distribution of the revenue per user (or “Revenue per 1K users” to be specific) and then calculate the probability that one distribution is “truly” better than the other one and not by a random chance — and this is what we call the Probability-to-be-the-best or P2BB (you can learn more about our approach [here](maths-behind-it)).
It’s important to note that while doing so, we only rely on the revenue that has been accumulated over the time the test has been running. So if you were to run a test comparing a yearly subscription to a weekly one, you would have to wait a really long time to truly understand what performs better. A similar thing happens when you compare trial subscriptions with non-trial subscriptions in an A/B test — as the active trials that could potentially shift the winner dynamics are always unaccounted for in the revenue.
This is where our predictive model comes into play. Having the current revenue distribution in an A/B test and trained on a large dataset it’s able to predict the future version of the revenue distribution (namely after 1 year). And after doing so, it produces a predicted P2BB — the one that you would arrive at if you were to run the test for the entire year.
Note that sometimes predicted P2BB can contradict the current P2BB. When that's the case, we highlight the variation rows with yellow like so:
We consider that a signal that you should accumulate more data to confirm the winner or dig deeper into the A/B test to find out the reason behind it. Generally we recommend trusting the predicted P2BB over the current P2BB because it simply takes more data into account, but the final decision is of course up to you.
### Model accuracy and certainty
The model achieves a high level of accuracy, with a Mean Absolute Percentage Error (MAPE) of slightly below 10%. This level of precision allows businesses to confidently rely on the model's predictions when making data-driven decisions.
To further ensure stability, the model employs a 'certainty' criterion based on three factors:
- A narrow prediction interval - the model is confident in its outcome
- Sufficient amount of subscriptions & revenue in the test
- At least 2 weeks from the test start have passed
To assure the quality of the prediction is of the highest standards possible, prediction is considered reliable only if at least two of these criteria are met without completely failing the third.
When a new A/B test begins, the model provides a year-ahead revenue per 1k (our main A/B test metric) prediction for each paywall. Predictions are displayed only when they meet the certainty criteria. If the data is insufficient, the model will indicate "insufficient data for prediction”.
### Limitations and considerations
While our predictive model is a powerful tool, it's important to consider its limitations.
The model's performance depends on the quality and representativeness of the available data. Unusual cohort behaviour or new apps not included in the training set can affect prediction accuracy.
Nevertheless, predictions are updated daily to reflect the latest data and user behaviors. This ensures that the insights you receive are always based on the most current information.
🚧 Note: This tool is a supplement to, not a replacement for, your expert judgment and understanding of your app's unique dynamics. Use these predictions as a guide alongside other metrics and market knowledge to make informed decisions.
---
# File: adapty-ads-manager
---
---
title: "Apple Ads Manager"
description: "Get realtime analytics from Apple Ads and manage and optimize your campaigns"
---
**Apple Ads Manager** is an all-in-one platform designed to help you manage, optimize, and scale your Apple Ads campaigns more efficiently. It connects your Apple Search Ads performance with key revenue metrics such as installs, trials, subscriptions, and lifetime value without requiring an MMP.
With real-time analytics, AI-driven forecasts, and smart automation, Apple Ads Manager eliminates tedious manual bid changes, spreadsheets, and guesswork, and replaces them with clear insights and tools that help you take action faster.
With Apple Ads Manager, you get:
- **Real-time performance data** across campaigns, ad groups, and keywords
- **End-to-end revenue tracking**: from search → install → trial → subscription → LTV
- **AI predictions & recommendations** for profitable scaling
- **Bulk management** of bids, budgets, statuses, and structures
- **Rule-based automations** to keep your CPA/ROAS targets stable
### Supported events
By default, Adapty sends three groups of events to User Acquisition:
- Trials
- Subscriptions
- Issues
You can check the full list of supported events [here](events.md).
## Step 2. Connect your ad platform and add tracking links
Adapty uses tracking links to connect app installs with campaign data.
You must use a tracking link as the destination URL in every ad campaign you want to measure in Adapty UA.
If you run ads on multiple platforms, set up tracking links for each platform separately.
There are two ways Adapty works with ad platforms:
- **Native integrations (Meta Ads, TikTok Ads).** Adapty connects directly to the ad platform. Tracking links are generated automatically, and campaign parameters are filled dynamically based on where the link is used. You can use the same link across different campaigns, ad sets, or creatives, and Adapty will automatically receive the correct campaign data and ad spend.
- **Tracking links only (all other ad platforms).** Adapty does not connect to the ad platform. Tracking links are created manually, and all campaign parameters must be defined explicitly when creating the link. Ad spend data is not available for these platforms.
3. In the Policy editor, paste the following JSON and change `adapty-s3-integration-test` to your bucket name:
```json showLineNumbers title="Json"
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListObjectsInBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::adapty-s3-integration-test"
},
{
"Sid": "AllowAllObjectActions",
"Effect": "Allow",
"Action": "s3:*Object",
"Resource": [
"arn:aws:s3:::adapty-s3-integration-test/*",
"arn:aws:s3:::adapty-s3-integration-test"
]
},
{
"Sid": "AllowBucketLocation",
"Effect": "Allow",
"Action": "s3:GetBucketLocation",
"Resource": "arn:aws:s3:::adapty-s3-integration-test"
}
]
}
```
4. After completing the policy configuration, you may choose to add tags (optional) and then click **Next** to proceed to the final step
5. In this step, you will name your policy and simply click on the **Create policy** button to finalize the creation process
#### 1.2. Create IAM user
To enable Adapty UA to upload raw data reports to your bucket, you will need to provide them with the Access Key ID and Secret Access Key for a user who has write access to the specific bucket.
1. Navigate to the IAM Console and select the [Users section](https://console.aws.amazon.com/iamv2/home#/users)
2. Click on the **Add users** button
3. Give the user a name, choose **Access key – Programmatic access**, and proceed to permissions
4. For the next step, please select the **Add user to group** option and then click the **Create group** button
5. Next, you need to assign a name to your User Group and select the policy that was previously created by you
6. Once you have selected the policy, click on the **Create group** button to complete the process
7. After successfully creating the group, please **select it** and proceed to the next step
8. Since this is the final step for this section, you may proceed by simply clicking on the **Create User** button
9. Lastly, you can either **download the credentials in .csv** format or alternatively, copy and paste the credentials directly from the dashboard
### Step 2. Configure integration in Adapty UA
1. Go to [**Integrations** -> **Amazon S3**](https://app.adapty.io/ua/integrations/s3)
2. Turn on the **Export install events to Amazon S3** toggle.
3. Fill out the following fields to build a connection between Amazon S3 and Adapty UA profiles:
| Field | Description |
|:-----------------------------| :----------------------------------------------------------- |
| **Access Key ID** | A unique identifier that is used to authenticate a user or application's access to an AWS service. Find this ID in the downloaded [csv file](ua-amazon-s3#how-to-create-amazon-s3-credentials) . |
| **Secret Access Key** | A private key that is used in conjunction with the Access Key ID to authenticate a user or application's access to an AWS service. Find this Key in the downloaded [csv file](ua-amazon-s3#how-to-create-amazon-s3-credentials) . |
| **S3 Bucket Name** | A globally unique name that identifies a specific S3 bucket within the AWS cloud. S3 buckets are a simple storage service that allows users to store and retrieve data objects, such as files and images, in the cloud. |
| **Folder Inside the Bucker** | The name of the folder that you want to have inside the selected S3 bucket. Please note that S3 simulates folders using object key prefixes, which are essentially folder names. |
| **Region** (Optional) | Get your Region from the AWS Management Console under your IAM user account. |
## Manual data export
In addition to the automatic event data export to Amazon S3, Adapty UA also provides a manual file export functionality. With this feature, you can select a specific date for the user acquisition data and export it to your S3 bucket manually. This allows you to have greater control over the data you export and when you export it.
## Table structure
In AWS S3 integration, Adapty UA provides a table to store historical data for installation events. The table contains information about the user profile, revenue and proceeds, and the origin store, among other data points.
:::warning
Note that this structure may grow over time — with new data being introduced by us or by the 3rd parties we work with. Make sure that your code that processes it is robust enough and relies on the specific fields, but not on the structure as a whole.
:::
Here is the table structure for the events:
| Column | Description |
|--------------------------|-------------------------------------------|
| `adapty_profile_id` | Unique Adapty profile identifier |
| `install_id` | Unique installation identifier |
| `created_at` | Record creation timestamp (ISO 8601) |
| `installed_at` | App installation timestamp (ISO 8601) |
| `store` | App store (`ios`, `android`) |
| `country` | User's country code (ISO 3166-1 alpha-2) |
| `ip_address` | Client IP address |
| `idfa` | iOS Identifier for Advertisers |
| `idfv` | iOS Identifier for Vendors |
| `gaid` | Google Advertising ID (Android) |
| `android_id` | Android device ID |
| `app_set_id` | Android App Set ID |
| `channel` | Attribution channel |
| `campaign_id` | Campaign identifier |
| `campaign_name` | Campaign name |
| `adset_id` | Ad set identifier |
| `adset_name` | Ad set name |
| `ad_id` | Ad identifier |
| `ad_name` | Ad name |
| `keyword_id` | Keyword identifier |
| `keyword_name` | Keyword name |
| `asa_org_id` | Apple Search Ads organization ID |
| `asa_keyword_match_type` | ASA keyword match type (`Exact`, `Broad`) |
| `asa_attribution` | ASA attribution data (JSON string) |
| `asa_conversion_type` | ASA conversion type |
| `asa_country_or_region` | ASA country or region |
| `asa_creative_set_name` | ASA creative set name |
| `fbclid` | Facebook Click ID |
| `ttclid` | TikTok Click ID |
| `utm_source` | UTM source parameter |
| `utm_medium` | UTM medium parameter |
| `utm_campaign` | UTM campaign parameter |
| `utm_term` | UTM term parameter |
| `utm_content` | UTM content parameter |
---
# File: ua-google-cloud-storage
---
---
title: "Google Cloud Storage"
description: "Integrate Google Cloud Storage with Adapty UA for secure user acquisition data storage."
---
Adapty UA's integration with Google Cloud Storage allows you to store user acquisition campaign data securely in one central location. You will be able to save your campaign performance data, attribution data, and user acquisition events to your Google Cloud Storage bucket as .csv files.
To set up this integration, you will need to follow a few simple steps in the Google Cloud Console and Adapty UA Dashboard.
:::note
Schedule
Adapty UA sends your data to Google Cloud Storage every 24h at 4:00 UTC.
Each file will contain data for the events created for the entire previous calendar day in UTC. For example, the data exported automatically at 4:00 UTC on March 8th will contain all the events created on March 7th from 00:00:00 to 23:59:59 in UTC.
:::
## How to set up Google Cloud storage integration
### Step 1. Create Google Cloud Storage credentials
This guide will help you create the necessary credentials in your Google Cloud Platform Console.
In order for Adapty UA to upload raw data reports to your designated bucket, the service account's key is required, as well as write access to the corresponding bucket. By providing the service account's key and granting write access to the bucket, you allow Adapty UA to securely and efficiently transfer the raw data reports from its platform to your storage environment.
:::warning
Please note that we only support Service Account HMAC key authorization, means it's essential to ensure that your Service Account HMAC key has the "Storage Object Viewer", "Storage Legacy Bucket Writer" and "Storage Object Creator" roles added to it to enable proper access to Google Cloud Storage.
:::
#### 2.1. Create Service Account
1. Go to the [IAM](https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts) section of your Google Cloud account and choose the relevant project or create a new one
2. Next, create a new service account for the Adapty UA by clicking on the "+ CREATE SERVICE ACCOUNT" button
3. Fill out the fields in the first step, as access will be granted at a later stage. In order to read more details about this page read the documentation [here](https://docs.cloud.google.com/iam/docs/service-accounts-create)
4. To create and download a [private JSON key](https://docs.cloud.google.com/iam/docs/keys-create-delete), navigate to the KEYS section and click on the "ADD KEY" button
5. In the DETAILS section, locate the Email value linked to the recently created service account and make a copy of it. This information will be necessary for the upcoming steps to authorize the account and allow it to write to the bucket
#### 2.2. Configure Bucket Permissions
6. Go to the Google Cloud Storage's[ Buckets](https://console.cloud.google.com/storage/browser) page and either select an existing bucket or create a new one to store the User Acquisition Data reports from Adapty UA
7. Navigate to the PERMISSIONS section and select the option to [GRANT ACCESS](https://docs.cloud.google.com/identity/docs/how-to?hl=en)
8. In the PERMISSIONS section, input the Email of the service account obtained in the fifth step mentioned earlier, then choose the Storage Object Creator role
9. Finally, click on SAVE to apply the changes
10. Remember to keep the name of the bucket for future reference
11. After passing these steps have successfully completed the necessary setup steps in the Google Cloud Console! The final step involves entering the bucket's name and downloading the JSON file for use in Adapty UA
### Step 2. Configure integration in Adapty UA
1. Go to [**Integrations** -> **Google Cloud Storage**](https://app.adapty.io/ua/integrations/google-cloud-storage)
2. Turn on the **Export install events to Google Cloud Storage** toggle
3. Fill out the required fields to build a connection between Google Cloud Storage and Adapty UA:
| Field | Description |
|:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Google Cloud service account key file** | The downloaded private [JSON key file](ua-google-cloud-storage#create-google-cloud-storage-credentials). |
| **Google Cloud bucket name** | The name of the bucket in Google Cloud Storage where you want to store your data. It should be unique within the Google Cloud Storage environment and should not contain any spaces. |
| **Folder inside the bucket** | The name of the folder inside the bucket where you want to store your data. It should be unique within the bucket and can be used to organize your data. This field is optional to fill. |
## Manual data export
In addition to the automatic event data export to Google Cloud Storage, Adapty UA also provides a manual file export functionality. With this feature, you can select a specific date for the user acquisition data and export it to your GCS bucket manually. This allows you to have greater control over the data you export and when you export it.
## Table structure
In Google Cloud Storage integration, Adapty UA provides a table to store historical data for installation events. The table contains information about the user profile, revenue and proceeds, and the origin store, among other data points.
:::warning
Note that this structure may grow over time — with new data being introduced by us or by the 3rd parties we work with. Make sure that your code that processes it is robust enough and relies on the specific fields, but not on the structure as a whole.
:::
Here is the table structure for the events:
| Column | Description |
|--------------------------|-------------------------------------------|
| `adapty_profile_id` | Unique Adapty profile identifier |
| `install_id` | Unique installation identifier |
| `created_at` | Record creation timestamp (ISO 8601) |
| `installed_at` | App installation timestamp (ISO 8601) |
| `store` | App store (`ios`, `android`) |
| `country` | User's country code (ISO 3166-1 alpha-2) |
| `ip_address` | Client IP address |
| `idfa` | iOS Identifier for Advertisers |
| `idfv` | iOS Identifier for Vendors |
| `gaid` | Google Advertising ID (Android) |
| `android_id` | Android device ID |
| `app_set_id` | Android App Set ID |
| `channel` | Attribution channel |
| `campaign_id` | Campaign identifier |
| `campaign_name` | Campaign name |
| `adset_id` | Ad set identifier |
| `adset_name` | Ad set name |
| `ad_id` | Ad identifier |
| `ad_name` | Ad name |
| `keyword_id` | Keyword identifier |
| `keyword_name` | Keyword name |
| `asa_org_id` | Apple Search Ads organization ID |
| `asa_keyword_match_type` | ASA keyword match type (`Exact`, `Broad`) |
| `asa_attribution` | ASA attribution data (JSON string) |
| `asa_conversion_type` | ASA conversion type |
| `asa_country_or_region` | ASA country or region |
| `asa_creative_set_name` | ASA creative set name |
| `fbclid` | Facebook Click ID |
| `ttclid` | TikTok Click ID |
| `utm_source` | UTM source parameter |
| `utm_medium` | UTM medium parameter |
| `utm_campaign` | UTM campaign parameter |
| `utm_term` | UTM term parameter |
| `utm_content` | UTM content parameter |
---
# File: configuration
---
---
title: "Configure 3d-party integration"
description: "Learn how to configure Adapty settings to optimize subscription management."
---
With Adapty integrations, you can seamlessly transmit subscription events and purchase data to your preferred platform or workflow. Whether you're seeking user behavior insights, customer engagement strategies, or enhanced product analytics for your marketing team, Adapty can effortlessly forward in-app purchase events to your chosen integration.
Adapty effortlessly tracks in-app purchases and subscription events such as trials, conversions, renewals, and cancellations. These [events](events) are automatically communicated to your chosen integrations. This allows you to engage with customers based on their current stage and analyze revenue-related activities within your app.
## Integration settings
Integrations offer the following configuration options that impact all events sent through this integration:
| Setting | Description |
|:--------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Reporting Proceeds** | Select how revenue values are presented: either net of App Store and Play Store commissions or gross (before deductions). Toggle the "Send sales as proceeds" checkbox to display sales as proceeds after App Store / Play Store commissions have been subtracted. |
| **Send Trial Price** | If checked, Adapty will transmit the subscription price for the Trial Started event. |
| **Exclude Historical Events** | Opt to exclude events that occurred before the user installed the app with Adapty SDK. This prevents duplication of events and ensures accurate reporting. For instance, if a user activated a monthly subscription on January 10th and updated the app with Adapty SDK on March 6th, Adapty will omit events before March 6th and retain subsequent events. |
| **Report User's Currency** | Choose whether sales are reported in the user's currency or in USD. |
| **Send User Attributes** | If you wish to send user-specific attributes, like language preferences, and your OneSignal plan supports more than 10 tags, select this option. Enabling this allows the inclusion of additional information beyond the default 10 tags. Note that exceeding tag limits may result in errors. |
| **Send Attributions** | Enable this option to transmit attribution information (e.g. AppsFlyer attribution) and receive relevant details. |
| **Send Play Store purchase token** | Enable this option to receive the Play Store token needed to revalidate the purchase if required. It will add the `play_store_purchase_token` parameter to the event. |
| **Delay events with future datetime** | **For AppsFlyer and custom webhooks only**: When enabled, renewal and trial conversion events are sent on the date they actually occur. When disabled (default), these events are sent immediately when detected, even if the date is in the future. |
| **Data residency** | **For Mixpanel and Amplitude only**: Select the data residency to determine where your events are processed and stored. |
## Configure the events
Below the credentials, there are three groups of events you can send to the selected integration platform from Adapty. You should turn on the ones you need.
It's important to note that event name customization is available for certain integrations, while for others, the event names are set and cannot be modified. Additionally, with certain integrations such as [Airbridge](airbridge#events-and-tags) for example, you have the flexibility to associate multiple event names with a single Adapty event. Check the full list of the Events offered by Adapty [here](events).
While we recommend utilizing Adapty's default event names, you have the freedom to adapt event names as per your specific requirements.
---
# File: events
---
---
title: "Events to send to 3d-party integrations"
description: "Track key subscription events using Adapty's analytics tools."
---
Apple and Google send subscription events directly to servers via [App Store Server Notifications](enable-app-store-server-notifications) and [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). As a result, mobile apps cannot reliably send events to analytics systems in real time. For example, if a user subscribes but never reopens the app, the developer won't receive any subscription status updates without a server.
Adapty bridges this gap by collecting subscription data and converting it into human-readable events. These integration events are sent in JSON format. While all events share the same structure, their fields vary depending on the event type, store, and specific configuration. You can find the exact fields included in each event on the respective integration pages.
To understand how to determine whether an event was successfully processed or if something went wrong, check the [Event statuses](event-statuses.md) page.
## Event types
Most events are created and sent to all configured integrations if they’re enabled. However, the **Access level updated** event only triggers if the [webhook integration](webhook) is configured and this event is enabled. This event will appear in the [Event Feed](https://app.adapty.io/event-feed) and will also be sent to the webhook, but it won’t be shared with other integrations.
If a webhook integration isn’t configured or this event type isn’t enabled, the **Access level updated** event won’t be created and won’t appear in the [Event Feed](https://app.adapty.io/event-feed).
| Event name | Description |
|:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| subscription_started | Triggered when a user activates a paid subscription without a trial period, meaning they are billed instantly. |
| subscription_renewed | Occurs when a subscription is renewed and the user is charged. This event starts from the second billing, whether it's a trial or non-trial subscription. |
| subscription_renewal_cancelled | A user has turned off subscription auto-renewal. The user retains access to premium features until the end of the paid subscription period. |
| subscription_renewal_reactivated | Triggered when a user reactivates subscription auto-renewal. |
| subscription_expired | Triggered when a subscription fully ends after being canceled. For instance, if a user cancels a subscription on December 12th but it remains active until December 31st, the event is recorded on December 31st when the subscription expires. |
| subscription_paused | Occurs when a user activates [subscription pause](https://developer.android.com/google/play/billing/subs#pause) (Android only). |
| subscription_deferred | Triggered when a subscription purchase is [deferred](https://adapty.io/glossary/subscription-purchase-deferral/), allowing users to delay payment while maintaining access to premium features. This feature is available through the Google Play Developer API and can be used for free trials or to accommodate users facing financial challenges. |
| non_subscription_purchase | Any non-subscription purchase, such as lifetime access or consumable products like in-game coins. |
| trial_started | Triggered when a user activates a trial subscription. |
| trial_converted | Occurs when a trial ends and the user is billed (first purchase). For example, if a user has a trial until January 14th but is billed on January 7th, this event is recorded on January 7th. |
| trial_renewal_cancelled | A user turned off subscription auto-renewal during the trial period. The user retains access to premium features until the trial ends but will not be billed or start a subscription. |
| trial_renewal_reactivated | Occurs when a user reactivates subscription auto-renewal during the trial period. |
| trial_expired | Triggered when a trial ends without converting to a subscription. |
| entered_grace_period | Occurs when a payment attempt fails, and the user enters a grace period (if enabled). The user retains premium access during this time. |
| billing_issue_detected | Triggered when a billing issue occurs during a charge attempt (e.g., insufficient card balance). |
| subscription_refunded | Triggered when a subscription is refunded (e.g., by Apple Support). |
| non_subscription_purchase_refunded | Triggered when a non-subscription purchase is refunded. |
| access_level_updated | Occurs when a user's access level is updated. |
The events above fully cover the users' state in terms of purchases. Let's look at some examples.
### Example 1
_The user activated a monthly subscription on April 1st with a 7-day trial. On the 4th day, he unsubscribed._
In that case, the following events will be sent:
1. `trial_started` on April 1st
2. `trial_renewal_cancelled` on 4th April
3. `trial_expired` on 7th April
### Example 2
_The user activated a monthly subscription on April 1st with a 7-day trial. On the 10th day, he unsubscribed._
In that case, the following events will be sent:
1. `trial_started` on April 1st
2. `trial_converted` on April 7th
3. `subscription_renewal_cancelled` on April 10th
4. `subscription_expired` on May 1st
For a detailed breakdown of which events are triggered in each scenario, check out the [Event flows](event-flows.md).
---
# File: event-flows
---
---
title: "Event flows"
description: "Discover detailed schemes of subscription event flows in Adapty. Learn how subscription events are generated and sent to integrations, helping you track key moments in your customers' journeys."
---
In Adapty, you'll receive various subscription events throughout a customer’s journey in your app. These subscription flows outline common scenarios to help you understand the events that Adapty generates as users subscribe, cancel, or reactivate subscriptions.
Keep in mind that Apple processes subscription payments several hours before the actual start/ renewal time. In the flows below, we show both the subscription start/ renewal and the charge happening at the same time to keep the diagrams clear.
Also, events related to the same action occur simultaneously and may appear in your **Event Feed** in any order, which might differ from the sequence shown in our diagrams.
## Subscription Lifecycle
### Initial Purchase Flow
This flow happens when a customer buys a subscription for the first time without a trial. In this situation, the following events are created:
- **Subscription started**
- **Access level updated** to grant access to the user
When the subscription renewal date comes, the subscription is renewed. In this case, the following events are created:
- **Subscription renewal** to start a new period of the subscription
- **Access level updated** to update the subscription expiration date, extending access for another period
Situations when the payment is not successful or when the user cancels the renewal are described in [Billing Issue Outcome Flow](event-flows#billing-issue-outcome-flow) and [Subscription Cancellation Flow](event-flows#subscription-cancellation-flow) ,respectively.
### Subscription Cancellation Flow
When a user cancels their subscription, the following events are created:
- **Subscription renewal canceled** to indicate that the subscription remains active until the end of the current period, after which the user will lose access
- The **Access level updated** event is created to disable auto-renewal for the access
Once the subscription ends, the **Subscription expired (churned)** event is triggered to mark the end of the subscription.
If a refund is approved, the following event replaces **Subscription expired (churned)**:
- **Subscription refunded** to end the subscription and provide details about the refund
For Stripe, a subscription can be canceled immediately, skipping the remaining period. In this case, all events are created simultaneously:
- **Subscription renewal cancelled**
- **Subscription expired (churned)**
- **Access Level updated** to remove the user’s access
If a refund is approved, a **Subscription refunded** event is also triggered when it’s approved.
### Subscription Reactivation Flow
If a user cancels a subscription, it expires, and they later repurchase the same subscription, a **Subscription renewed** event will be created. Even if there’s a gap in access, Adapty treats this as a single transaction chain, linked by the `vendor_original_transaction_id`. So, the repurchase is considered a renewal.
The **Access level updated** events will be created twice:
- at the subscription end to revoke the user's access
- at the subscription repurchase to grant access
### Subscription Pause Flow (Android only)
This flow applies when a user pauses and later resumes a subscription on Android.
Pausing a subscription has delayed effects. If a user pauses a subscription before it is set to renew, the subscription remains active, and the user retains paid access for the remainder of the billing period.
1. When the user pauses a subscription, they trigger the **Subscription paused (Android only)** event.
2. At the end of the subscription period Adapty triggers the **Access level updated** event to revoke the user's access.
3. When the user resumes the subscription, the following events are triggered:
- **Subscription renewed**
- **Access level updated** to restore the user's access
These subscriptions will belong to the same transaction chain, linked with the same **vendor_original_transaction_id**.
## Trial Flows
If you use trials in your app, you’ll receive additional trial-related events.
### Trial with Successful Conversion Flow
The most common flow occurs when a user starts a trial, provides a credit card, and successfully converts to a standard subscription at the end of the trial period. In this situation, the following events are created at the moment of teh trial start:
- **Trial started** to mark the trial start
- **Access level updated** to grant access
The **Trial converted** event is created when the standard subscription starts.
### Trial without Successful Conversion Flow
If a user cancels the trial before it converts to a subscription, the following events are created at the time of cancellation:
- **Trial renewal cancelled** to disable automatic conversion of the trial to a subscription
- **Access level updated** to disable access renewal
The user will have access until the end of the trial when the **Trial expired** event is created to mark the trial's end.
### Subscription Reactivation after Expired Trial Flow
If a trial expires (due to a billing issue or cancellation) and the user later buys a subscription, the following events are created:
- **Access level updated** to grant access to the user
- **Trial converted**
Even with a gap between the trial and subscription, Adapty links the two using `vendor_original_transaction_id`. This conversion is treated as part of a continuous transaction chain, starting with a zero-price trial. That is why the **Trial converted** event is created rather than the **Subscription started**.
## Product Changes
This section covers any adjustments made to active subscriptions, such as upgrades, downgrades or purchases of a product from another group.
### Immediate Product Change Flow
After a user changes a product, it can be changed in the system immediately before the subscription ends (mostly in case of an upgrade or replacement of a product). In this case, at the moment of the product change:
- The access level is changed, and two **Access level updated** events are created:
1. to remove the access to the first product.
2. to give access to the second product.
- The old subscription ends, and a refund is paid (the **Subscription refunded** event is created with the `cancellation_reason` = `upgraded`). Please note that no **Subscription expired (churned)** event is created; the **Subscription refunded** event replaces it.
- The new subscription starts (the **Subscription started** event is created for the new product).
If a user downgrades the subscription, the first subscription will last till the end of the paid period, and when the subscription ends, it will be replaced with a new, lower-tier subscription. In this situation, only the **Access level updated** event to disable access autorenewal will be created at once. All other events will be created at the moment of the subscription's actual replacement:
- Another **Access level updated** event is created to give access to the second product.
- The **Subscription expired (churned)** event is created to end the subscription for the first product.
- The **Subscription started** event is created to start a new subscription for the new product.
### Delayed Product Change Flow
There is also a variant when a user changes the product at the moment of the subscription renewal. This variant is very similar to the previous one: one **Access level updated** event will be created at once to disable access autorenewal for the old product. All other events will be created at the moment when the user changes the subscription and it is changed in the system:
- Another **Access level updated** event is created to give access to the second product.
- The **Subscription expired (churned)** event is created to end the subscription for the first product.
- The **Subscription started** event is created to start a new subscription for the new product.
## Billing Issue Outcome Flow
If attempts to convert a trial or renew a subscription fail due to a billing issue, what happens next depends on whether a grace period is enabled.
With a grace period, if the payment succeeds, the trial converts or the subscription renews. If it fails, the app store will continue to attemp to charge the user for the subscription and if still fails, the app store will end the trial or subscription itself.
Therefore, at the moment of the billing issue, the following events are created in Adapty:
- **Billing issue detected**
- **Entered grace period** (if the grace period is enabled)
- **Access level updated** to provide the access till the end of the grace period
If the payment succeeds later, Adapty records a **Trial converted** or **Subscription renewed** event, and the user does not lose access.
If the payment ultimately fails and the app store cancels the subscription, Adapty generates these events:
- **Trial expired** or **Subscription expired (churned)** with `cancellation_reason: billing_error`
- **Access level updated** to revoke the user's access
Without a grace period, the Billing Retry Period (the period when the app store continues to attempt to charge the user) starts immediately.
If the payment never succeeds till the end of the grace period, the flow is the same: the same events are created when the app store ends the subscription automatically:
- **Trial expired** or **Subscription expired (churned)** event with a `cancellation_reason` of `billing_error`
- **Access level updated** to revoke the user's access
## Sharing Purchases Across User Accounts Flows
When a
Here’s a breakdown of the fields related to access level assignment and transferring in the events generated in this scenario:
- **User A: Access level updated (sent when User A purchases a subscription in the app)**
```json showLineNumbers
{
"profile_id": "00000000-0000-0000-0000-000000000000",
"customer_user_id": UserA,
"event_properties": {
"profile_has_access_level": true,
},
"profiles_sharing_access_level": null
}
```
- **User A: Access level updated (sent when the app is reinstalled and User B logs in, revoking User A's access)**
```json showLineNumbers
{
"profile_id": "00000000-0000-0000-0000-000000000000",
"customer_user_id": UserA,
"event_properties": {
"profile_has_access_level": false,
},
"profiles_sharing_access_level": null
}
```
- **User B: Access level updated (sent when User B logs in and access is granted)**
```json showLineNumbers
{
"profile_id": "00000000-0000-0000-0000-000000000001",
"customer_user_id": UserB,
"event_properties": {
"profile_has_access_level": true,
},
"profiles_sharing_access_level": null
}
```
### Shared Access Between Users Flow
This option allows multiple users to share the same access level if their device is signed in to the same Apple/Google ID. This is useful when a user reinstalls the app and logs in with a different email — they'll still have access to their previous purchase. With this option, multiple identified users can share the same access level. While the access level is shared, all transactions are logged under the original
Here’s a breakdown of the fields related to access level assignment and sharing in the events generated in this scenario:
**User B: Access level updated (sent when User B logs in and access is granted)**
```json showLineNumbers
{
"profile_id": "00000000-0000-0000-0000-000000000000",
"customer_user_id": UserA,
"event_properties": {
"profile_has_access_level": true,
},
"profiles_sharing_access_level": [
{
"profile_id": "00000000-0000-0000-0000-000000000001,
"customer_user_id": UserB
}
]
}
```
### Access Not Shared Between Users Flow
With this option, only the first user profile to receive the access level retains it permanently. This is ideal if purchases need to be tied to a single
---
# File: event-statuses
---
---
title: "Integration event statuses"
description: ""
---
Adapty determines deliverability based on the HTTP status code, considering any response outside the `200-399` range as a failure.
You can track the status of integration events in the **Event List** within the Adapty Dashboard. The system displays statuses for all enabled integrations, regardless of whether a specific event type is turned on for a given integration.
- Black: The event was successfully sent.
- Grey: The event type is disabled for this integration.
- Red: There is an issue with the integration that requires attention.
For more details on failed events, hover over the integration name to see a tooltip with specific error information.
The **Event Feed** displays data from the past two weeks to optimize performance. This limitation improves page loading speed, making it easier for users to navigate and analyze events efficiently.
---
# File: adjust
---
---
title: "Adjust"
description: "Connect Adjust with Adapty for better subscription tracking and analytics."
---
[Adjust](https://www.adjust.com/) is one of the leading Mobile Measurement Partner (MMP) platforms that collects and presents data from marketing campaigns. This helps companies track their campaign performance.
Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. Therefore, this integration allows you to track subscription events in Adjust and analyze precisely how much revenue your campaigns generate.
The integration between Adapty and Adjust works in two main ways.
1. **Adapty receives attribution data from Adjust**
Once you've set up the Adjust integration, Adapty will start receiving attribution data from Adjust. You can easily access and view this data on the user's profile page.
2. **Adapty sends subscription events to Adjust**
Adapty can send all subscription events which are configured in your integration to Adjust. As a result, you'll be able to track these events within the Adjust dashboard. This integration is beneficial for evaluating the effectiveness of your advertising campaigns.
## Set up integration
### Connect Adapty to Adjust
1. Open the Adapty Dashboard, and navigate to [Integrations > Adjust](https://app.adapty.io/integrations/adjust).
2. Set the toggle on top of the page to "on".
3. Fill out the fields, and set your access credentials.
3. If you enabled OAuth authorization on the Adjust platform, it is mandatory to provide an **OAuth Token** during the integration process for your iOS and Android apps.
4. Next, provide the **app tokens** for your iOS and Android apps. Open your Adjust dashboard and you'll see your apps.
:::note
You may have different Adjust applications for iOS and Android, so in Adapty you have two independent sections for that. If you have only one Adjust app, just fill in the same information.
:::
5. Select your app from the list and copy the **App Token**. Paste the token into the corresponding field on the Adapty dashboard.
### Configure events and tags
Adjust works a bit differently from other platforms. You need to manually create events in Adjust dashboard, get event tokens, and copy-paste them to appropriate events in Adapty.
So the first step here is to find event tokens for all events that you want Adapty to send. To do that:
1. In the Adjust dashboard, open your app and switch to the **Events** tab.
1. Copy the event token and paste it to Adapty. Below the credentials, there are three groups of events you can send to Adjust from Adapty. Check the full list of the events offered by Adapty [here](events).
Adapty will send subscription events to Adjust using a server-to-server integration, allowing you to view all subscription events in your Adjust dashboard and link them to your acquisition campaigns.
:::important
Consider the following:
- Adjust doesn't support events older than 58 days. So, if you have an event that is more than 58 days old, Adapty will send it to Adjust, but the event datetime will be replaced by the current timestamp.
- Adjust doesn’t support IPv6. If you disable IP collection in the SDK in **App settings** or on the SDK activation, only a backend IPv6 may be sent, and tracking can fail — keep SDK IP collection enabled to ensure IPv4 is used.
:::
### Connect your app to Adjust
After you complete the steps described above, add the following two methods to your app. They will establish communication between your app and Adjust:
1. **To send subscription data to Adjust**: Pass the Adjust device ID to the `setIntegrationIdentifier()` SDK method
2. **To receive attribution data from Adjust**: Update attribution data with the `updateAttribution()` SDK method
For Adjust version 5.0 or later, use the following example:
They both can be found in your Airbridge dashboard in the [Third-party Integrations > Adapty](https://app.airbridge.io/app/testad/integrations/third-party/adapty) section.
Adapty API token field is pre-generated on the Adapty backend. You will need to copy the value of Adapty API token and paste it into the Airbridge Dashboard in the Adapty Authorization Token field.
### Configure events and tags
Below the credentials, there are three groups of events you can send to Airbridge from Adapty
Simply turn on the ones you need.
### Connect your app to Airbridge
For the integration, you should pass `airbridge_device_id` to profile builder and call `setIntegrationIdentifier` as it is shown in the example below:
## Set up integration
### Connect Adapty to the AdServices framework
Apple Ads via [AdServices](https://developer.apple.com/documentation/adservices) does require some configuration in Adapty Dashboard, and you will also need to enable it on the app side. To set up Apple Ads using the AdServices framework through Adapty, follow these steps:
#### Step 1: Configure Info.plist
Add `AdaptyAppleSearchAdsAttributionCollectionEnabled` to the app’s `Info.plist` file and set it to `YES` (boolean value).
#### Step 2: Obtain public key
In the Adapty Dashboard, navigate to [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads)
Locate the pre-generated public key (Adapty provides a key pair for you) and copy it.
:::note
If you're using an alternative service or your own solution for Apple Ads attribution, you can upload your own private key.
:::
#### Step 3: Configure user management on Apple Ads
In your [Apple Ads account](https://ads.apple.com/app-store) go to **Settings > User Management** page. In order for Adapty to fetch attribution data you need to invite another Apple ID account and grant it API Account Manager access. You can use any account you have access to or create a new one just for this purpose. The important thing is that you must be able to log into Apple Ads using this Apple ID.
#### Step 4: Generate API credentials
As a next step, log in to the newly added account in Apple Ads. Navigate to Settings -> API in the Apple Ads interface. Paste the previously copied public key into the designated field. Generate new API credentials.
#### Step 5: Configure Adapty with Apple Ads credentials
Copy the Client ID, Team ID, and Key ID fields from the Apple Ads settings. In the Adapty Dashboard, paste these credentials into the corresponding fields.
### Connect your app to the AdServices network
Once you complete [the AdServices framework setup](#connect-the-adservices-framework), Adapty automatically starts collecting Apple Search Ad attribution data. You don't need to add any SDK code.
For iOS applications, this attribution data will **always** take priority over data from other sources. If this behaviour is unwanted, *disable* ASA attribution with the instructions below.
## Disable integration
To turn Apple Search Ads attribution off, open the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads), and toggle the **Receive Apple Search Ads attribution** switch.
:::warning
Please note that disabling this will completely stop the reception of ASA analytics. As a result, ASA will no longer be used in analytics or sent to integrations. Additionally, SplitMetrics Acquire and Asapty will cease to function, as they rely on ASA attribution to operate correctly.
The attribution received before this change will not be affected.
:::
## Uploading your own keys
:::note
Optional
These steps are not required for Apple Ads attribution, only for working with other services like Asapty or your own solution.
:::
You can use your own public-private key pair if you are using other services or own solution for ASA attribution.
### Step 1
Generate private key in Terminal
```text showLineNumbers title="Text"
openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
```
Upload it in Adapty Settings -> Apple Ads (Upload private key button)
### Step 2
Generate public key in Terminal
```text showLineNumbers title="Text"
openssl ec -in private-key.pem -pubout -out public-key.pem
```
You can use this public key in your Apple Ads settings of account with API Account Manager role. So you can use generated Client ID, Team ID, and Key ID values for Adapty and other services.
---
# File: switch-from-appsflyer-s2s-api-2-to-3
---
---
title: "Switch from AppsFlyer S2S API 2 to 3"
description: "Upgrade from AppsFlyer S2S API 2 to 3 in Adapty."
---
According to the [official AppsFlyer What's New](https://support.appsflyer.com/hc/en-us/articles/20509378973457-Bulletin-Upgrading-the-AppsFlyer-S2S-API), to provide a more secure experience for API usage and to reduce fraud, AppsFlyer has upgraded its server-to-server (S2S) API for in-app events. The existing endpoint will be deprecated in the future and we recommend to start planning the switch.
Adapty supports AppsFlyer S2S API 3 and provides you with a seamless switch from API 2. Keep in mind that this switch is one-way, so you won’t be able to return to API 2 once you’ve made the change.
To switch from AppsFlyer S2S API 2 to 3:
1. Open the [AppsFlyer site](https://www.appsflyer.com/home) and log in.
2. Click **Your account name** -> **Security Center** in the top-left corner of the dashboard.
3. In the **Manage your account security** window, click the **Manage your AppsFlyer API and S2S tokens** button.
4. If you do not have an S2S token, click the **New token** button. If you have it, please proceed with step 8.
5. In the **New token** window, enter the name of the token. This name is solely for your reference.
6. Choose **S2S** in the **Choose type** list.
7. Don't forget to click the **Create new token** button to save the new token.
8. In the **Tokens** window, copy the S2S token.
9. Open [**Integrations** -> **AppsFlyer**](https://app.adapty.io/integrations/appsflyer) in the Adapty Dashboard.
10. In the **AppsFlyer S2S API** field, select **API 3**.
11. Paste the copied S2S key into the **Dev key for iOS** and **Dev key for Android** fields.
12. Click the **Save** button to confirm the switch.
At this moment, your integration instantly switches to AppsFlyer S2S API 3 and your new events will be sent to the new URL: `https://api3.appsflyer.com/inappevent`.
---
# File: asapty
---
---
title: "Asapty"
description: "Discover Asapty and its role in Adapty’s subscription ecosystem."
---
Using [Asapty](https://asapty.com/) integration you can optimize your Search Ads campaigns. Adapty sends subscription events to Asapty, so you can build custom dashboards there, based on Apple Search Ads attribution.
This specific integration doesn't add any attribution data to Adapty, as we already have everything we need from [ASA](apple-search-ads) directly.
## Set up integration
### Connect Adapty to Asapty
To integrate Asapty navigate to [Integrations > Asapty](https://app.adapty.io/integrations/asapty) in the Adapty dashboard and fill out the field value for Asapty ID.
Asapty ID can be found in Settings> General section in your Asapty account.
### Configure events and tags
Below the credentials, there are three groups of events you can send to Asapty from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events).
We recommend using the default event names provided by Asapty. But you can change the event names based on your needs.
### Connect your app to Asapty
Once you complete the steps outlined above, Adapty automatically receives attribution data from Asapty. There's no need to explicitly request attribution data in your application code. For better attribution data accuracy, configure Asapty to share the `customerUserId` with each event's data.
## Asapty event structure
Adapty sends events to Asapty via a GET request using query parameters. Each event URL looks like this:
```
https://asapty.com/_api/mmpEvents/?source=adapty&asaptyid=a1b2c3d4&keywordid=12345&adgroupid=67890&campaignid=11223&conversiondate=1709294400000&event_name=subscription_renewed&install_time=1709100000&app_name=MyApp&json=%7B%22af_revenue%22%3A%229.99%22%2C%22af_currency%22%3A%22USD%22...%7D
```
Query parameters:
| Parameter | Type | Description |
|:-----------------|:-------|:-----------------------------------------------------|
| `source` | String | Always "adapty". |
| `asaptyid` | String | The Asapty ID from your credentials. |
| `keywordid` | String | Apple Search Ads Keyword ID (if available). |
| `adgroupid` | String | Apple Search Ads Ad Group ID (if available). |
| `campaignid` | String | Apple Search Ads Campaign ID (if available). |
| `conversiondate` | Long | Timestamp of the event in **milliseconds**. |
| `event_name` | String | The event name (mapped from Adapty event). |
| `install_time` | Long | Timestamp of the install in seconds. |
| `app_name` | String | The app title from Adapty (if available). |
| `json` | String | URL-encoded JSON string containing event details (see below). |
The `json` parameter is a URL-encoded JSON string containing the following fields:
| Parameter | Type | Description |
|:--------------------------|:-------|:---------------------------------------------|
| `af_revenue` | String | Revenue amount as a string. |
| `af_currency` | String | Currency code (e.g., "USD"). |
| `transaction_id` | String | Store Transaction ID. |
| `original_transaction_id` | String | Original Store Transaction ID. |
| `purchase_date` | Long | Purchase timestamp in milliseconds. |
| `original_purchase_date` | Long | Original purchase timestamp in milliseconds. |
| `environment` | String | `Production` or `Sandbox`. |
| `vendor_product_id` | String | The Product ID from the store. |
| `profile_country` | String | Country code based on user's IP. |
| `store_country` | String | Country code of the store user. |
## Troubleshooting
- Make sure you've configured [Apple Search Ads](apple-search-ads) in Adapty and [uploaded credentials](https://app.adapty.io/settings/apple-search-ads), without them, Asapty won't work.
- Only the profiles with detailed, non-organic ASA attribution will deliver their events to Asapty. You will see "The user profile is missing the required integration data." if the attribution is not sufficient.
- Profiles created prior to configuring the integrations will not be able to deliver their events to Asapty.
- If the integration with Adapty isn't working despite the correct setup, ensure the **Receive Apple Search Ads attribution in Adapty** toggle is enabled in the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads).
---
# File: branch
---
---
title: "Branch"
description: "Integrate Branch with Adapty to track deep links and app conversions."
---
[Branch](https://www.branch.io/) enables customers to reach, interact, and assess results across diverse devices, channels, and platforms. It's a user-friendly platform designed to enhance mobile revenue through specialized links that work seamlessly on all devices, channels, and platforms.
Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective.
The integration between Adapty and Branch operates in two main ways.
1. **Receiving attribution data from Branch**
Once you've set up the Branch integration, Adapty will start receiving attribution data from Branch. You can easily access and view this data on the user's profile page.
2. **Sending subscription events to Branch**
Adapty can send all subscription events which are configured in your integration to Branch. As a result, you'll be able to track these events within the Branch dashboard.
## Set up integration
### Connect Adapty to Branch
To integrate Branch go to [Integrations > Branch](https://app.adapty.io/integrations/branch) in Adapty Dashboard , turn on a toggle from off to on, and fill out fields.
To get the value for the **Branch Key**, open your Branch [Account Settings](https://dashboard.branch.io/account-settings/profile) and find the **Branch Key** field. Use it for the **Key test** (for Sandbox) or **Key live** (for Production) field in the Adapty Dashboard. In Branch, switch between Live and Tests environments for the appropriate key.
### Configure events and tags
Below the credentials, there are three groups of events you can send to Branch from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events).
You can send an event with Proceeds \(after Apple/Google cut\) or just revenue. Also, you can check a box for reporting in the user's currency.
We recommend using the default event names provided by Adapty. But you can change the event names based on your needs.
Adapty will send subscription events to Branch using a server-to-server integration, allowing you to view all subscription events in your Branch dashboard and link them to your acquisition campaigns.
### Connect your app to Branch
1. Call the `.setIntegrationIdentifier()` SDK method to initialize the connection. You can pass your Branch Identity ID to the `customerUserId` parameter.
1. To find App ID, open your app page in [App Store Connect](https://appstoreconnect.apple.com/), go to the **App Information** page in **General** section, and find **Apple ID** in the left bottom part of the screen.
2. You need an application on [Meta for Developers](https://developers.facebook.com/) platform. Log in to your app and then find advanced settings. You can find the **App ID** in the header.
3. Disable client-side tracking in your Meta SDK configuration to prevent double counting of revenue in Meta Ads Manager. You can find this setting in your Meta Developer Console under **App Settings > Advanced Settings**. Set **Log in-app events automatically** to "No". This will ensure that revenue events are only tracked through Adapty's integration.
To track install and usage events, you'll need to activate Meta SDK in your code. You can find implementation details in the Meta SDK documentation for your platform:
- [iOS SDK](https://developers.facebook.com/docs/ios/getting-started)
- [Android SDK](https://developers.facebook.com/docs/android/getting-started)
- [Unity SDK](https://developers.facebook.com/docs/unity/getting-started/canvas)
You can use this integration with Android apps as well. If you set up Android SDK configuration in the **App Settings**, setting up the **Facebook App ID** is enough.
### Configure events and tags
Please note that the Facebook Ads integration specifically caters to companies using Meta for ad campaigns and optimizing them based on customer behavior. It supports Meta's standard events for optimization purposes. Consequently, modifying the event name is not available for the Meta Ads integration. Adapty effectively maps your customer events to their corresponding Meta events for accurate analysis.
| Adapty event | Meta Ads event |
| :---------------------------- | :-------------------------- |
| Subscription initial purchase | Subscribe |
| Subscription renewed | Subscribe |
| Subscription cancelled | CancelSubscription |
| Trial started | StartTrial |
| Trial converted | Subscribe |
| Trial cancelled | CancelTrial |
| Non subscription purchase | fb_mobile_purchase |
| Billing issue detected | billing_issue_detected |
| Entered grace period | entered_grace_period |
| Auto renew off | auto_renew_off |
| Auto renew on | auto_renew_on |
| Auto renew off subscription | auto_renew_off_subscription |
| Auto renew on subscription | auto_renew_on_subscription |
StartTrial, Subscribe, CancelSubscription are standard events.
To enable specific events, simply toggle on the ones you require. In case multiple event names are selected, Adapty will consolidate the data from all the chosen events into a single Adapty event name.
### Connect your app to Facebook Ads
If you follow the steps above, Facebook will automatically receive subscription data from Adapty.
Following the changes to IDFA in iOS 14.5, we recommend that you request the user's `facebookAnonymousId` from Facebook. That way, if the user's IDFA is unavailable, the integration will continue to function. Follow the
Below the credentials, there are three groups of events you can send to Singular from Adapty. Check the full list of the events offered by Adapty [here](events).
We recommend using the default event names provided by Adapty. But you can change the event names based on your needs.
Adapty will send subscription events to Singular using a server-to-server integration, allowing you to view all subscription events in your Singular dashboard and link them to your acquisition campaigns.
:::warning
Profiles created prior to configuring the integrations will not be able to deliver their events to Singular.
:::
### Connect your app to Singular
The integration between Adapty and Singular is server-so-server. As such, there's no need to add any extra code to your application.
## Event structure
Adapty sends events to Singular via a GET request using query parameters. Each event is structured like this:
```json
{
"n": "subscription_renewed",
"a": "singular_sdk_key_123",
"p": "iOS",
"i": "com.example.app",
"ip": "192.168.100.1",
"idfa": "00000000-0000-0000-0000-000000000000",
"idfv": "00000000-0000-0000-0000-000000000000",
"ve": "17.0.1",
"att_authorization_status": 3,
"custom_user_id": "user_12345",
"utime": 1709294400,
"amt": 9.99,
"cur": "USD",
"purchase_product_id": "yearly.premium.6999",
"purchase_transaction_id": "GPA.3383...",
"e": "{\"is_revenue_event\":true,\"amt\":9.99,\"cur\":\"USD\",\"purchase_product_id\":\"yearly.premium.6999\",\"purchase_transaction_id\":\"GPA.3383...\"}"
}
```
Where:
| Parameter | Type | Description |
|:---------------------------|:--------|:-----------------------------------------------------|
| `n` | String | The event name (mapped from Adapty event). |
| `a` | String | Your Singular SDK Key. |
| `p` | String | Platform ("iOS" or "Android"). |
| `i` | String | Store App ID (Bundle ID). |
| `ip` | String | User's IP address. |
| `idfa` | String | **iOS only**. ID for Advertisers (uppercase). |
| `idfv` | String | **iOS only**. ID for Vendors (uppercase). |
| `aifa` | String | **Android only**. Google Advertising ID (lowercase). |
| `andi` | String | **Android only**. Android ID (lowercase). |
| `asid` | String | **Android only**. App Set ID (lowercase). |
| `ve` | String | OS version. |
| `att_authorization_status` | Integer | **iOS only**. ATT status (e.g., `3` for authorized). |
| `custom_user_id` | String | The user's Customer User ID. |
| `utime` | Long | UNIX timestamp of the event in seconds. |
| `amt` | Float | Revenue amount. |
| `cur` | String | Currency code (e.g., "USD"). |
| `purchase_product_id` | String | The Product ID from the store. |
| `purchase_transaction_id` | String | Original transaction ID. |
| `e` | String | JSON string containing event details (see below). |
The `e` parameter (custom event data) is a JSON-encoded string containing:
| Parameter | Type | Description |
|:--------------------------|:--------|:--------------------------------------|
| `is_revenue_event` | Boolean | `true` if the event contains revenue. |
| `amt` | Float | Revenue amount. |
| `cur` | String | Currency code. |
| `purchase_product_id` | String | The Product ID from the store. |
| `purchase_transaction_id` | String | Original transaction ID. |
---
# File: tenjin
---
---
title: "Tenjin integration"
description: ""
---
Tenjin is a mobile attribution and analytics platform for app developers and marketers. It provides tools to measure and optimize user acquisition campaigns by offering detailed insights into app performance and user behavior. With its transparent and flexible approach, Tenjin aggregates data from advertising networks and app stores, enabling teams to analyze ROI, track conversions, and monitor key performance metrics.
By forwarding [subscription events](events) to Tenjin, you can see exactly where conversions come from and which campaigns bring in the most value across all channels, platforms, and devices. Essentially, Tenjin dashboards offer advanced analytics for marketing campaigns.
By forwarding Tenjin's attribution to Adapty, you enrich the Adapty analytics with additional filtration criteria you can use in cohort and conversion analysis.
This integration operates in two key ways:
1. **Receiving attribution data from Tenjin**
Once integrated, Adapty collects attribution data from Tenjin. You can access this information on the user’s profile page in the Adapty Dashboard.
2. **Sending subscription events to Tenjin**
Adapty sends purchase events to Tenjin in real-time. These events help evaluate the effectiveness of your ad campaigns directly within Tenjin’s dashboard.
| Integration characteristic | Description |
| -------------------------- | ------------------------------------------------------------ |
| Schedule | Real-time |
| Data direction | Two-way transmission:
3. Log into the [Tenjin Dashboard](https://tenjin.com/).
4. Go to **Configuration** -> **Apps** in the navigation menu.
5. Select the app for your platform (iOS or Android) and navigate to the **App and SDK** tab.
6. In the **App and SDK** tab, click **Copy** in the **SDK Key** column. If you don't have an SDK key yet, click the **Generate SDK Key** button to create one.
7. Return to the Adapty Dashboard and paste the copied SDK Key into the appropriate platform field:
- For iOS apps: paste into the **iOS SDK Key** or **iOS Sandbox SDK Key** field
- For Android apps: paste into the **Android SDK Key** or **Android Sandbox SDK Key** field
:::info
Tenjin doesn't have a specific Sandbox mode for server-to-server integration. Use a separate Tenjin app or the same key for both production and sandbox events.
:::
8. If you have apps on both platforms, repeat steps 5-7 for your other platform.
9. (optional) Adjust the **How the revenue data should be sent** section if needed. For a detailed explanation of its settings, refer to the [Integration settings](configuration#integration-settings).
10. Click **Save** to finalize the setup.
Adapty will now send purchase events to Tenjin and receive attribution data. You can adjust event sharing in the **Events names** section.
### Configure events and tags
Tenjin only accepts purchase and **Trial started** events. In the **Events names** section, select which events to share with Tenjin to align with your tracking goals.
### Connect your app to Tenjin
Use the `Adapty.updateAttribution()` SDK method to retrieve attribution data from Tenjin, and pass it on to Adapty.
2. Toggle on **Amplitude integration** to enable it.
3. Fill in the integration fields:
| Field | Description |
| ------------------------------------------ | ------------------------------------------------------------ |
| **Amplitude iOS/ Android/ Stripe API key** | Enter the Amplitude **API Key** for iOS/ Android/ Stripe into Adapty. Locate it under **Project settings** in Amplitude. For help, check [Amplitude docs](https://amplitude.com/docs/apis/authentication). Start with **Sandbox** keys for testing, then switch to **Production** keys after successful tests. |
4. Optional settings for further customization:
| Parameter | Description |
| --------------------------------------- | ------------------------------------------------------------ |
| **How the revenue data should be sent** | Choose whether to send gross revenue or revenue after taxes and commissions. See [Store commission and taxes](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) for details. |
| **Exclude historical events** | Choose to exclude events before Adapty SDK installation, preventing duplicate data. For example, if a user subscribed on January 10th but installed the Adapty SDK on March 6th, Adapty will only send events from March 6th onward. |
| **Send User Attributes** | Select this option to send user-specific attributes like language preferences. |
| **Always populate user_id** | Adapty automatically sends `device_id` as `amplitudeDeviceId`. For `user_id`, this setting defines behavior:
We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. Adapty will send subscription events to Amplitude using a server-to-server integration, allowing you to view all subscription events in your Amplitude dashboard.
### SDK configuration
Use the `setIntegrationIdentifier()` method to set the `amplitude_device_id` parameter. It's a must to set up the integration.
If you have a user registration, you can pass `amplitude_user_id` as well.
4. Go to [Integrations > AppMetrica](https://app.adapty.io/integrations/appmetrica) in the Adapty Dashboard
5. Paste your AppMetrica credentials.
### Events and tags
Adapty allows you to send three groups of events to AppMetrica. You can enable the events you need to track your app's performance. For a complete list of available events, see our [events documentation](events).
:::note
AppMetrica syncs events every 4 hours, so there may be a delay before events appear in your dashboard.
:::
:::tip
We recommend using Adapty's default event names for consistency, but you can customize them to match your existing analytics setup.
:::
### Revenue settings
By default, Adapty sends revenue data as properties in events, which appear in AppMetrica's Events report. You can configure how this revenue data is calculated and displayed:
- **Revenue calculation**: Choose how revenue values are calculated to match your financial reporting needs:
- **Gross revenue**: Shows the total revenue before any deductions, useful for tracking the full amount customers pay
- **Proceeds after store commission**: Displays revenue after App Store/Play Store fees are deducted, helping you track actual earnings
- **Proceeds after store commission and taxes**: Shows net revenue after both store fees and applicable taxes, providing the most accurate picture of your earnings
- **Report user's currency**: When enabled, sales are reported in the user's local currency, making it easier to analyze revenue by region. When disabled, all sales are converted to USD for consistent reporting across different markets.
- **Send revenue events**: Enable this option to make revenue data appear not only in the Events report but also in AppMetrica's [In-app and ad revenue](https://appmetrica.yandex.com/docs/en/mobile-reports/revenue-report) report. Make sure you’re not sending revenue from anywhere else, as this may result in duplication.
- **Exclude historical events**: When enabled, Adapty won't send events that occurred before the user installed the app with Adapty SDK. This helps avoid data duplication if you were already sending events to analytics before integrating Adapty.
### SDK configuration
To enable the AppMetrica integration in your app, you need to set up two identifiers:
1. `appmetrica_device_id`: Required for basic integration
2. `appmetrica_profile_id`: Optional, but recommended if your app has user registration
Use the `setIntegrationIdentifier()` method to set these values. Here's how to implement it for each platform:
### 2\. Integrate with Adapty
Then Adapty needs your Firebase App ID and Google Analytics API Secret to send events and user properties. You can find these parameters in the Firebase Console and Google Analytics Data Streams Tab respectively.
Next, access the App's Stream details page within the Data Streams section of Admin settings in [Google Analytics.](https://analytics.google.com/analytics/web/#/)
Under **Additional settings**, go to the **Measurement Protocol API secrets** page and create a new **API Secret** if it doesn't exist. Copy the value.
Then, your next step will be adjusting integration in Adapty Dashboard. You will need to provide Firebase App ID and Google Analytics API Secret to us for your iOS, Android, and/or Stripe platforms.
:::note
If you are using the Stripe integration, consider its limitations in the dedicated [guide](stripe#current-limitations). These limitations will apply to the Firebase integration as well.
:::
## SDK configuration
:::important
For the integration to work, ensure you add Firebase to your app first:
- [iOS](https://firebase.google.com/docs/ios/setup)
- [Android](https://firebase.google.com/docs/android/setup)
- [React Native](https://firebase.google.com/docs/web/setup)
- [Flutter](https://firebase.google.com/docs/flutter/setup)
- [Unity](https://firebase.google.com/docs/unity/setup)
:::
Then you have to set up Adapty SDK to associate your users with Firebase. For each user, you should send the `firebase_app_instance_id` to Adapty. Here you can see an example of the code that can be used to integrate Firebase SDK and Adapty SDK.
You can see that some events have designated names, for example. "Purchase", while other ones are usual Adapty events. This discrepancy comes from[ Google Analytics event types](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events). Currently, supported events are [Refund](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#refund%22%3ERefund) and [Purchase](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#purchase%22%3EPurchase). Other events are custom events. So, please ensure that your event names are [supported](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=firebase#limitations%22%3E) by Google Analytics.
Also, you can set up sending user properties in the Adapty dashboard.
This means that your events will be enriched with `subscription_state` and `subscription_product_id` by Adapty. But you also have to [enable](https://support.google.com/analytics/answer/14240153?hl=en) this feature in Google Analytics. So to use **User properties** in your analytics, begin by assigning them to a custom dimension through the Firebase Console's **Custom Definitions** by selecting the **User scope**, naming, and describing them.
Please check that your user property names are `subscription_state` and `subscription_product_id`. Otherwise, we won't be able to send you subscription status data.
And that's all! Wait for new insights from Google.
## Troubleshooting
### Data discrepancy
If there is a data discrepancy between Adapty and Firebase, that might occur because not all your users use the app version that has the Adapty SDK. To ensure the data consistency, you can force your users to update the app to a version with the Adapty SDK.
Additionally, sandbox events are sent to Firebase by default, and this cannot be disabled. So, in situations when an app has a few Production events and a lot of Sandbox ones, there can be a notable discrepancy in numbers between Adapty’s Analytics and Firebase.
### Events are shown as delivered in Adapty but not available in Firebase
There is a time delay between when events are sent from Adapty and when they appear on the Google Analytics Dashboard. It's suggested to monitor the Realtime Dashboard on your Google Analytics account to see the latest events in real-time.
---
# File: mixpanel
---
---
title: "Mixpanel"
description: "Connect Mixpanel with Adapty for powerful subscription analytics."
---
[Mixpanel](https://mixpanel.com/home/) is a powerful product analytics service. Its event-driven tracking solution empowers product teams to get valuable insights into optimal user acquisition, conversion, and retention strategies across different platforms.
This integration enables you to bring all the Adapty events into Mixpanel. As a result, you'll gain a more comprehensive insight into your subscription business and customer actions. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective.
## How to set up Mixpanel integration
1. Open the [Integrations -> Mixpanel](https://app.adapty.io/integrations/mixpanel) page in the Adapty Dashboard.
2. Enable the toggle and enter your **Mixpanel Token**. You can specify a token for all platforms or limit it to specific platforms if you only want to receive data from certain ones.
### Finding Your Mixpanel Token
To get your **Mixpanel Token**:
1. Log in to your [Mixpanel Dashboard](https://mixpanel.com/settings/project/).
2. Open **Settings** and select **Organization Settings**.
3. From the left sidebar, go to **Projects** and select your project.
## How the integration works
Adapty automatically maps relevant event properties—such as user ID and revenue—to [Mixpanel-native properties](https://docs.mixpanel.com/docs/data-structure/user-profiles). This ensures accurate tracking and reporting of subscription-related events.
Additionally, Adapty accumulates revenue data per user and updates their [User Profile Properties](https://docs.mixpanel.com/docs/data-structure/user-profiles), including `subscription state` and `subscription product ID`. Once an event is received, Mixpanel updates the corresponding fields in real time.
## Events and tags
Below the credentials, there are three groups of events you can send to Mixpanel from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events).
We recommend using the default event names provided by Adapty. But you can change the event names based on your needs.
## SDK configuration
Use `.setIntegrationIdentifier()` method to set `mixpanelUserId`. If not set, Adapty uses your user ID (`customerUserId`) or if it's null Adapty ID. Make sure that the user id you use to send data to Mixpanel from your app is the same one you send to Adapty.
2. Log into the [PostHog Dashboard](https://posthog.com/).
3. Navigate to **Settings -> Project**.
4. In the **Project** window, scroll down to the **Project ID** section and copy the **Project API key**.
5. Paste the API key into the **Project API key** field in the Adapty Dashboard. PostHog doesn’t have a specific Sandbox mode for server-to-server integration.
6. Choose your **PostHog Deployment**:
| Option | Description |
| ------ | ------------------------------------------------------------ |
| us/eu | Default PostHog-hosted deployments. |
| Custom | For self-hosted instances. Enter your instance URL in the **PostHog Instance URL** field. |
7. (optional) If you're using a self-hosted PostHog deployment, enter your deployment's address in the **PostHog Instance URL** field.
8. (optional) Tweak settings like **Reporting Proceeds**, **Exclude Historical Events**, **Report User's Currency**, and **Send Trial Price**. Check the [Integration settings](https://adapty.io/docs/configuration#integration-settings) for details on these options.
9. (optional) You can also customize which events are sent to PostHog in the **Events names** section. Disable unwanted events or rename them as needed.
10. Click **Save** to finalize the setup.
## SDK configuration
To enable receiving attribution data from PostHog, pass the `distinctId` value to Adapty as shown below:
Open your SplitMetrics Acquire account, hover over one of the MMP logos, and click the **Settings** button. Find your Client ID in the dialog under item **5**, copy it, and then paste it to Adapty as **Client ID**.
You will also have to set Apple App ID to use the integration. To find your App ID, open your app page in App Store Connect, go to the **App Information page** in the **General** section, and find the **Apple ID** in the left bottom part of the screen.
## Events and tags
Below the credentials, there are three groups of events you can send to SplitMetrics Acquire from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events).
We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. Adapty will send subscription events to SplitMetrics Acquire using a server-to-server integration, allowing you to view all subscription events in your SplitMetrics dashboard.
## SDK configuration
You don't have to configure anything on the SDK side, but we recommend sending `customerUserId` to Adapty for better accuracy.
:::warning
Make sure you've configured [Apple Search Ads](apple-search-ads) in Adapty and [uploaded credentials](https://app.adapty.io/settings/apple-search-ads), without them, SplitMetrics Acquire won't work.
:::
## Troubleshooting
If the integration with SplitMetrics Acquire isn't working despite the correct setup:
- Make sure you've enabled the **Receive Apple Search Ads attribution in Adapty** toggle is enabled in the [App Settings -> Apple Search Ads tab](https://app.adapty.io/settings/apple-search-ads), configured [Apple Search Ads](apple-search-ads) in Adapty, and [uploaded credentials](https://app.adapty.io/settings/apple-search-ads), without them, SplitMetric won't work.
- Ensure the profiles have non-organic ASA attribution. Only profiles with detailed, non-organic ASA attribution will deliver their events to Asapty.
## SplitMetrics Acquire event structure
Adapty sends events to SplitMetrics Acquire via a GET request using query parameters. Each event is structured like this:
```json
{
"source": "Apple Search Ads",
"app_id": "123456789",
"name": "subscription_renewed",
"type": "subscription_renewed",
"revenue": 9.99,
"currency": "USD",
"tap_time": "2024-03-01 12:00:00",
"open_time": "2024-03-01 12:05:00",
"event_time": "2024-03-02 12:00:00",
"adaccount_id": "123456",
"campaign_id": "123456789",
"adgroup_id": "123456789",
"keyword_id": "123456789",
"creative_set_id": "123456789",
"Ad_id": "123456789",
"country_or_region": "US",
"conversion_type": "Download",
"user_id": "user_12345",
"att_status": "3",
"device_type": "iphone",
"app_version": "1.2.3",
"sdk_version": "2.10.0",
"ios_version": "17.2",
"event_value": "{\"vendor_product_id\":\"yearly.premium.6999\",\"original_transaction_id\":\"GPA.3383...\"}",
"event_id": "123e4567-e89b-12d3-a456-426614174000"
}
```
Where:
| Parameter | Type | Description |
|:--------------------|:-------|:-----------------------------------------------------------------------------------------------------------|
| `source` | String | Always "Apple Search Ads". |
| `app_id` | String | Apple App ID. |
| `name` | String | Event name (mapped from Adapty event). |
| `type` | String | Event type (same as `name`). |
| `revenue` | Float | Revenue amount. |
| `currency` | String | Currency code. |
| `tap_time` | String | Date and time of the ad tap. |
| `open_time` | String | Date and time of the app open (install). |
| `event_time` | String | Date and time of the event. |
| `adaccount_id` | String | ASA Organization ID. |
| `campaign_id` | String | ASA Campaign ID. |
| `adgroup_id` | String | ASA Ad Group ID. |
| `keyword_id` | String | ASA Keyword ID. |
| `creative_set_id` | String | ASA Creative Set ID. |
| `Ad_id` | String | ASA Ad ID. |
| `country_or_region` | String | Store country or region. |
| `conversion_type` | String | Conversion type (e.g., "Download"). |
| `user_id` | String | Customer User ID or Adapty Profile ID. |
| `att_status` | String | Tracking usage status (0-3). |
| `device_type` | String | Device type (e.g., "iphone", "ipad"). |
| `app_version` | String | Application version. |
| `sdk_version` | String | Adapty SDK version. |
| `ios_version` | String | iOS version. |
| `event_value` | String | JSON string containing all available [event details](webhook-event-types-and-fields#for-most-event-types). |
| `event_id` | String | Unique event ID (UUID). |
---
# File: braze
---
---
title: "Braze"
description: "Integrate Braze with Adapty for seamless customer engagement and push notifications."
---
As one of the top customer engagement solutions, [Braze](https://www.braze.com/) provides a wide range of tools for push notifications, email, SMS, and in-app messaging. By integrating Adapty with Braze, you can easily access all of your subscription events in one place, giving you the ability to trigger automated communication based on those events.
Adapty provides a complete set of data that lets you track [subscription events](events) from all stores in one place and can be used to update your users' profiles in Braze. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. Therefore, this integration allows you to track subscription events in your Braze dashboard and map them with your [acquisition campaigns.](https://www.braze.com/product/journey-orchestration)
Adapty sends subscription events, user properties and purchases over to Braze, so you can build target communication with customers using Braze push notifications after a short and easy integration as described below.
## How to set up Braze integration
To integrate Braze go to [Integrations -> Braze](https://app.adapty.io/integrations/braze), switch on the toggle, and fill out the fields.
The initial step of the integration process is to provide the necessary credentials to establish a connection between your Braze and Adapty profiles. You will need the **REST API Key**, your **Braze Instance ID**, and **App IDs** for iOS and Android for the integration to work properly:
1. **REST API Key** can be created in **Braze Dashboard** → **Settings** → **API Keys**. Make sure your key has a `users.track` permission when creating it:
2. To get **Braze Instance ID** note your Braze Dashboard URL and go to the section of [Braze Docs](https://www.braze.com/docs/api/basics/#endpoints) where the instance ID is specified. It should have a regional form such as US-03, EU-01, etc.
3. iOS and Android App IDs can be found in Braze Dashboard → Settings → API Keys as well. Copy them from here:
## Events, user attributes and purchases
Below the credentials, there are three groups of events you can send to Braze from Adapty. Simply turn on the ones you need. You may also change the names of the events as you need to send it to Braze. Check the full list of the Events offered by Adapty [here](events):
Adapty will send subscription events and user attributes to Braze using a server-to-server integration, allowing you to view it in your Braze Dashboard and configure campaigns based on that.
For events that have revenue, such as trial conversions and renewals, Adapty will send this info to Braze as purchases.
[Here](messaging#event-properties) you can find the complete specifications for the event properties sent to Braze.
:::note
Helpful user attributes
Adapty sends some user attributes for Braze integration by default. You can refer to the list of them provided below to determine which is best suited for your needs.
:::
| User attribute | Type | Value |
|--------------|----|-----|
| `adapty_customer_user_id` | String | Contains the value of the unique identifier of the user defined by the customer. Can be found both in the Adapty [Dashboard](profiles-crm) and in Braze. |
| `adapty_profile_id` | String | Contains the value of the unique identifier Adapty User Profile ID of the user, which can be found in the Adapty [Dashboard](profiles-crm). |
| `environment` | String | Indicates whether the user is operating in a sandbox or production environment.
Values are either `Sandbox` or `Production`
| | `store` | String |Contains the name of the Store that used to make the purchase.
Possible values:
`app_store` or `play_store`.
| | `vendor_product_id` | String |Contains the value of Product Id in Apple/Google store.
e.g., org.locals.12345
| | `subscription_expires_at` | String |Contains the expiration date of the latest subscription.
Value format is:
YYYY-MM-DDTHH:mm:ss.SSS+TZ
e.g., 2023-02-15T17:22:03.000+0000
| | `active_subscription` | String | The value will be set to `true` on any purchase/renewal event, or `false` if the subscription is expired. | | `period_type` | String |Indicates the latest period type for the purchase or renewal.
Possible values are
`trial` for a trial period or `normal` for the rest.
| All float values will be rounded to int. Strings stay the same. In addition to the pre-defined list of tags available, it is possible to send [custom attributes](segments#custom-attributes) using tags. This allows for more flexibility in the type of data that can be included with the tag and can be useful for tracking specific information related to a product or service. All custom user attributes are sent automatically to Braze if the user marks the ** Send user attributes** checkbox from [the integration page](https://app.adapty.io/integrations/braze). ## SDK Configuration To link user profiles in Adapty and Braze you need to either configure Braze SDK with the same customer user ID as Adapty or use its `.changeUser()` method:
2. Enable the integration toggle.
3. Enter your **OneSignal App ID**.
To set up the integration with OneSignal, go to [Integrations -> OneSignal](https://app.adapty.io/integrations/onesignal) in your Adapty dashboard, turn on a toggle, and configure the integration credentials.
## Retrieving your OneSignal App ID
Find your **OneSignal App ID** in your [OneSignal Dashboard](https://dashboard.onesignal.com/login):
1. Navigate to **Settings** → **Keys & IDs**.
2. Copy your **OneSignal App ID** and paste it into the **App ID** field in the Adapty Dashboard.
You can find more information about the OneSignal ID in the [following documentation.](https://documentation.onesignal.com/docs/en/keys-and-ids)
### Configuring events
Adapty allows you to send three groups of events to OneSignal. Toggle on the ones you need in the Adapty Dashboard. You can view the complete list of available events with detailed description [here](events).
Adapty sends subscription events to OneSignal using a server-to-server integration, allowing you to track all subscription-related activity in OneSignal.
:::warning
Starting April 17, 2023, OneSignal's Free Plan no longer supports this integration. It is available only on **Growth**, **Professional**, and **higher** plans. For details, see [OneSignal Pricing](https://onesignal.com/pricing).
:::
## Custom tags
This integration updates and assigns various properties to your Adapty users as tags, which are then sent to OneSignal. Refer to the list of tags below to find the ones that best fit your needs.
:::warning
OneSignal has a tag limit. This includes both Adapty-generated tags and any existing tags in OneSignal. Exceeding the limit may cause errors when sending events.
:::
| Tag | Type | Description |
|---|----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `adapty_customer_user_id` | String | The unique identifier of the user in your app. It must be consistent across your system, Adapty, and OneSignal. |
| `adapty_profile_id` | String | The Adapty user profile ID, available in your [Adapty Dashboard](profiles-crm). |
| `environment` | String | `Sandbox` or `Production`, indicating the user’s current environment. |
| `store` | String | Store where the product was bought. Options: **app_store**, **play_store**, **stripe**, or the name of your [custom store](custom-store). |
| `vendor_product_id` | String | The product ID in the app store (e.g., `org.locals.12345`). |
| `subscription_expires_at` | String | Expiration date of the latest subscription (`YYYY-MM-DDTHH:MM:SS+0000`, e.g., `2023-02-10T17:22:03.000000+0000`). |
| `last_event_type` | String | The latest event type from the [Adapty event list](events).
1. **App ID** can be found in your Pushwoosh dashboard.
2. **Auth token **can be found in the API Access section in Pushwoosh Settings.
## Events and tags
Below the credentials, there are three groups of events you can send to Pushwoosh from Adapty. Simply turn on the ones you need. You may also change the names of the events as you need to send it to Pushwoosh. Check the full list of the Events offered by Adapty [here](events).
Adapty will send subscription events to Pushwoosh using a server-to-server integration, allowing you to view all subscription events in your Pushwoosh Dashboard.
:::note
Custom tags
With Adapty you can also use your custom tags for Pushwoosh integration. You can refer to the list of tags provided below to determine which tag is best suited for your needs.
:::
| Tag | Type | Value |
|---|----|-----|
| `adapty_customer_user_id` | String | Contains the value of the unique identifier of the user, which can be found on the Pushwoosh side. |
| `adapty_profile_id` | String | Contains the value of the unique identifier Adapty User Profile ID of the user, which can be found in your Adapty [dashboard](profiles-crm). |
| `environment` | String | Indicates whether the user is operating in a sandbox or production environment.
Values are either `Sandbox` or `Production`
| | `store` | String |Contains the name of the Store that used to make the purchase.
Possible values:
`app_store` or `play_store`.
| | `vendor_product_id` | String |Contains the value of Product ID in the Apple/Google store.
e.g., org.locals.12345
| | `subscription_expires_at` | String |Contains the expiration date of the latest subscription.
Value format is:
year-month dayThour:minute:second
e.g., 2023-02-10T17:22:03.000000+0000
| | `last_event_type` | String | Indicates the type of the last received event from the list of the standard [Adapty events](events) that you have enabled for the integration. | | `purchase_date` | String |Contains the date of the last transaction (original purchase or renewal).
Value format is:
year-month dayThour:minute:second
e.g., 2023-02-10T17:22:03.000000+0000
| | `original_purchase_date` | String |Contains the date of the first purchase according to the transaction.
Value format is:
year-month dayThour:minute:second
e.g., 2023-02-10T17:22:03.000000+0000
| | `active_subscription` | String | The value will be set to `true` on any purchase/renewal event, or `false` if the subscription is expired. | | `period_type` | String |Indicates the latest period type for the purchase or renewal.
Possible values are
`trial` for a trial period or `normal` for the rest.
| All float values will be rounded to int. Strings stay the same. In addition to the pre-defined list of tags available, it is possible to send [custom attributes](segments#custom-attributes) using tags. This allows for more flexibility in the type of data that can be included with the tag and can be useful for tracking specific information related to a product or service. All custom user attributes are sent automatically to Pushwoosh if the user marks the ** Send user custom attributes** checkbox from[ the integration page](https://app.adapty.io/integrations/pushwoosh) ## SDK configuration To link Adapty with Pushwoosh, you need to send us the `HWID` value:
2. Give it any name (`Adapty` for example) and add it to your workspace:
### 2\. Give permission to post and get a token for your app
You'll be redirected to your app's page in Slack.
1. Scroll down and click **Permissions**:
2. After the redirect, scroll down to **Scopes** and click **Add an OAuth Scope**:
3. Give `chat:write`, `chat:write.public` and `chat:write.customize` permissions. Those are needed to post in your channels and customize the messages:
4. Scroll back to the top of the page and click **Install to Workspace**:
5. Click **Allow** here:
After this, you'll be redirected to the same page, but you'll have an OAuth Token available (`xoxb-...`). This is exactly what's needed to complete the setup:
### 3\. Configure the integration in Adapty
1. Go to [**Integrations** → **Slack**](https://app.adapty.io/integrations/slack):
2. Paste the `xoxb-...` token from the previous step and choose which channels the app will post to. You can set up the integration to receive events only on production, sandbox or both. You can also choose which currency to post in (original or converted to USD).
:::note
Note that if you'd like to post messages from Adapty in a private channel, you'll need to manually add the `Adapty` app you've created in Slack to that channel. Otherwise, it would not work.
:::
3. Finally, you can choose which events you'd like to receive under **Events**:
You're all set!
The events will be sent to the channels you've specified. You'll be able to see the revenue where applicable and view the customer profile in Adapty:
---
# File: s3-exports
---
---
title: "Amazon S3"
description: "Export subscription data to S3 for advanced analytics and reporting."
---
Adapty's integration with Amazon S3 allows you to store event and paywall visit data securely in one central location. You will be able to save your [subscription events](events) to your Amazon S3 bucket as .csv files.
To set up this integration, you will need to follow a few simple steps in the AWS Console and Adapty dashboard.
:::note
Schedule
Adapty sends your data every **24h** at 4:00 UTC.
Each file will contain data for the events created for the entire previous calendar day in UTC. For example, the data exported automatically at 4:00 UTC on March 8th will contain all the events created on March 7th from 00:00:00 to 23:59:59 in UTC.
:::
## How to set up Amazon S3 integration
To start receiving data, you'll need the following credentials:
1. Access key ID
2. Secret access key
3. S3 bucket name
4. Folder name inside the S3 bucket
:::note
Nested directories
You can specify nested directories in the Amazon S3 bucket name field, e.g. adapty-events/com.sample-app
:::
To integrate Amazon S3 go to [**Integrations** -> **Amazon S3**](https://app.adapty.io/integrations/s3), turn on a toggle from off to on, and fill out fields.
First of all set credentials to build a connection between Amazon S3 and Adapty profiles.
In the Adapty Dashboard, the following fields are needed to set up the connection:
| Field | Description |
| :--------------------------- | :----------------------------------------------------------- |
| **Access Key ID** | A unique identifier that is used to authenticate a user or application's access to an AWS service. Find this ID in the downloaded [csv file](s3-exports#how-to-create-amazon-s3-credentials) . |
| **Secret Access Key** | A private key that is used in conjunction with the Access Key ID to authenticate a user or application's access to an AWS service. Find this Key in the downloaded [csv file](s3-exports#how-to-create-amazon-s3-credentials) . |
| **S3 Bucket Name** | A globally unique name that identifies a specific S3 bucket within the AWS cloud. S3 buckets are a simple storage service that allows users to store and retrieve data objects, such as files and images, in the cloud. |
| **Folder Inside the Bucker** | The name of the folder that you want to have inside the selected S3 bucket. Please note that S3 simulates folders using object key prefixes, which are essentially folder names. |
## How to create Amazon S3 credentials
This guide will help you create the necessary credentials in your AWS Console.
### 1\. Create Access Policy
First, navigate to the [IAM Policy Dashboard](https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/policies) in your AWS Console and select the option to **Create Policy**.
In the Policy editor, paste the following JSON and change `adapty-s3-integration-test` to your bucket name:
```json showLineNumbers title="Json"
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListObjectsInBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::adapty-s3-integration-test"
},
{
"Sid": "AllowAllObjectActions",
"Effect": "Allow",
"Action": "s3:*Object",
"Resource": [
"arn:aws:s3:::adapty-s3-integration-test/*",
"arn:aws:s3:::adapty-s3-integration-test"
]
},
{
"Sid": "AllowBucketLocation",
"Effect": "Allow",
"Action": "s3:GetBucketLocation",
"Resource": "arn:aws:s3:::adapty-s3-integration-test"
}
]
}
```
After completing the policy configuration, you may choose to add tags (optional) and then click **Next** to proceed to the final step. In this step, you will name your policy and simply click on the **Create policy** button to finalize the creation process.
### 2\. Create IAM user
To enable Adapty to upload raw data reports to your bucket, you will need to provide them with the Access Key ID and Secret Access Key for a user who has write access to the specific bucket.
To proceed with that, navigate to the IAM Console and select the [Users section](https://console.aws.amazon.com/iamv2/home#/users). From there, click on the **Add users** button.
Give the user a name, choose **Access key – Programmatic access**, and proceed to permissions.
For the next step, please select the **Add user to group** option and then click the **Create group** button.
Next, you need to assign a name to your User Group and select the policy that was previously created by you. Once you have selected the policy, click on the **Create group** button to complete the process.
After successfully creating the group, please **select it** and proceed to the next step.
Since this is the final step for this section, you may proceed by simply clicking on the **Create User** button.
Lastly, you can either **download the credentials in .csv** format or alternatively, copy and paste the credentials directly from the dashboard.
## Manual data export
In addition to the automatic event data export to Amazon S3, Adapty also provides a manual file export functionality. With this feature, you can select a specific time interval for the event data and export it to your S3 bucket manually. This allows you to have greater control over the data you export and when you export it.
The specified date range will be used to export the events created from Date A 00:00:00 UTC up to Date B 23:59:59 UTC.
## Table structure
In AWS S3 integration, Adapty provides a table to store historical data for transaction events and paywall visits. The table contains information about the user profile, revenue and proceeds, and the origin store, among other data points. Essentially, these tables log all transactions generated by an app for a given time period.
:::warning
Note that this structure may grow over time — with new data being introduced by us or by the 3rd parties we work with. Make sure that your code that processes it is robust enough and relies on the specific fields, but not on the structure as a whole.
:::
Here is the table structure for the events:
| Column | Description |
|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **profile_id** | Adapty user ID. |
| **event_type** | Lower cased event name. Refer to the [Events](events) section to learn event types. |
| **event_datetime** | ISO 8601 date. |
| **transaction_id** | A unique identifier for a transaction such as a purchase or renewal. |
| **original_transaction_id** | The transaction identifier of the original purchase. |
| **subscription_expires_at** | The Expiration date of subscription. Usually in the future. |
| **environment** | Could be Sandbox or Production. |
| **revenue_usd** | Revenue in USD. Can be empty. |
| **proceeds_usd** | Proceeds in USD. Can be empty. |
| **net_revenue_usd** | Net revenue (income after taxes) in USD. Can be empty. |
| **tax_amount_usd** | Amount of money deducted for taxes in USD. Can be empty. |
| **revenue_local** | Revenue in local currency. Can be empty. |
| **proceeds_local** | Proceeds in local currency. Can be empty. |
| **net_revenue_local** | Net revenue (income after taxes) in local currency. Can be empty. |
| **tax_amount_local** | Amount of money deducted for taxes in local currency. Can be empty. |
| **customer_user_id** | Developer user ID. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. |
| **store** | Could be _app_store_ or _play_store_. |
| **product_id** | Product ID in the Apple App Store, Google Play Store, or Stripe. |
| **base_plan_id** | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. |
| **developer_id** | Developer (SDK) ID of the paywall where the transaction originated. |
| **ab_test_name** | Name of the A/B test where the transaction originated. |
| **ab_test_revision** | Revision of the A/B test where the transaction originated. |
| **paywall_name** | Name of the paywall where the transaction originated. |
| **paywall_revision** | Revision of the paywall where the transaction originated. |
| **profile_county** | Profile Country determined by Adapty, based on IP. |
| **install_date** | ISO 8601 date when the installation happened. |
| **idfv** | [identifierForVendor](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor) on iOS devices |
| **idfa** | [advertisingIdentifier](https://developer.apple.com/documentation/adsupport/asidentifiermanager/advertisingidentifier) on iOS devices |
| **advertising_id** | The Advertising ID is a unique code assigned by the Android Operating System that advertisers might use to uniquely identify a user's device |
| **ip_address** | Device IP (can be IPv4 or IPv6, with IPv4 preferred when available). It is updated each time IP of the device changes. |
| **cancellation_reason** | A reason why the user canceled a subscription.
Can be:
**iOS & Android** _voluntarily_cancelled_, _billing_error_, _refund_
**iOS** _price_increase_, _product_was_not_available_, _unknown_, _upgraded_
**Android** _new_subscription_replace_, _cancelled_by_developer_
| | **android_app_set_id** | An [AppSetId](https://developer.android.com/design-for-safety/privacy-sandbox/reference/adservices/appsetid/AppSetId) - unique, per-device, per developer-account user-resettable ID for non-monetizing advertising use cases. | | **android_id** | On Android 8.0 (API level 26) and higher versions of the platform, a 64-bit number (expressed as a hexadecimal string), unique to each combination of app-signing key, user, and device. For more details, see [Android developer documentation](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). | | **device** | The end-user-visible device model name. | | **currency** | The 3-letter currency code (ISO-4217) of the transaction. | | **store_country** | Profile Country determined by Apple/Google store. | | **attribution_source** | Attribution source. | | **attribution_network_user_id** | ID assigned to the user by attribution source. | | **attribution_status** | Can be organic, non_organic or unknown. | | **attribution_channel** | Marketing channel name. | | **attribution_campaign** | Marketing campaign name. | | **attribution_ad_group** | Attribution ad group. | | **attribution_ad_set** | Attribution ad set. | | **attribution_creative** | Attribution creative keyword. | | **attributes** | JSON of [custom user attributes](setting-user-attributes#custom-user-attributes). This will include any custom attributes you’ve set up to send from your mobile app. To send it, enable the **Send User Attributes** option in the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. | | **integration_ids** | All integration IDs associated with a profile. Dictionary. Example: {'mixpanel_user_id': 'mixpanelUserId-test', 'facebook_anonymous_id': 'facebookAnonymousId-test'} | Here is the table structure for the paywall visits: | Column | Description | | :-------------------- | :----------------------------------------------------------------------------------------------------------- | | **profile_id** | Adapty user ID. | | **customer_user_id** | Developer user ID. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. | | **profile_country** | Profile Country determined by Apple/Google store. | | **install_date** | ISO 8601 date when the installation happened. | | **store** | Could be _app_store_ or _play_store_. | | **paywall_showed_at** | The date when the paywall has been displayed to the customer. | | **developer_id** | Developer (SDK) ID of the paywall where the transaction originated. | | **ab_test_name** | Name of the A/B test where the transaction originated. | | **ab_test_revision** | Revision of the A/B test where the transaction originated. | | **paywall_name** | Name of the paywall where the transaction originated. | | **paywall_revision** | Revision of the paywall where the transaction originated. | ## Events and tags You can manage what data is communicated by the integration. The integration offers the following configuration options: | Setting | Description | | :--------------------------------- | :----------------------------------------------------------- | | **Exclude Historical Events** | Opt to exclude events that occurred before the user installed the app with Adapty SDK. This prevents duplication of events and ensures accurate reporting. For instance, if a user activated a monthly subscription on January 10th and updated the app with Adapty SDK on March 6th, Adapty will omit events before March 6th and retain subsequent events. | | **Include events without profile** | Opt to include transactions that are not linked to a user profile in Adapty. These may include purchases made before Adapty SDK was installed or transactions received from store server notifications that cannot be immediately associated with a specific user. | | **Send User Attributes** | If you wish to send user-specific attributes, like language preferences, and your OneSignal plan supports more than 10 tags, select this option. Enabling this allows the inclusion of additional information beyond the default 10 tags. Note that exceeding tag limits may result in errors. |
Below the integration settings, there are three groups of events you can export, send, and store in Amazon S3 from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events).
---
# File: google-cloud-storage
---
---
title: "Google Cloud Storage"
description: "Integrate Google Cloud Storage with Adapty for secure data storage."
---
Enable the Google Cloud Storage integration to securely store [subscription events](events) and [paywall visit data](paywall-metrics) in one central location: your Google Cloud Storage bucket.
Every day at 4AM UTC, Adapty will upload .csv files with the previous day's data to your buckets. You can choose whether you want to receive **event** data, **paywall visit** data, or **both**. You can also export this data [manually](#manual-data-export) at any time, for any time period.
To set up the integration, [generate a bucket access key](#create-google-cloud-storage-credentials) in your Google Cloud console, and [add it to your Adapty settings](#set-up-google-cloud-storage-integration).
## Upload schedule and duration
Adapty uploads data to Google Cloud Storage every 24 hours, at 04\:00 UTC.
The files contain data for events created during the previous calendar day (UTC). The file uploaded on March 8th will contain all the events created on March 7th, from 00\:00\:00 to 23\:59\:59 UTC.
The process may take up to several hours, depending on the total number of files in the queue, as well as the amount of data you personally requested. If Adapty includes historical data with your first upload, it will take longer than the subsequent daily uploads.
## Set up Google Cloud storage integration
You need to have a valid Google Cloud service account key with **write access**. To generate it, follow the steps in the [create credentials](#create-google-cloud-storage-credentials) section.
:::warning
You can use different buckets with different credentials for events and paywall visits. However, if **either** set of credentials is invalid, [**both uploads will fail**](#troubleshooting).
:::
Go to [**Integrations** -> **Google Cloud Storage**](https://app.adapty.io/integrations/google-cloud-storage), and open the necessary tab (**Events** or **Paywall visits**). Enable the integration.
Upload the file with your **Google Cloud service account key**. Specify the target **bucket** and **folder**. Save your changes.
### Optional settings for event data
You can specify which events to include in the report, and set custom names for the events. See the [events](events) article for the full list of available events.
| Name | Default | Description |
| ------------------------------ | ----------------- | ----------- |
| Exclude historical events | true | Exclude information about events that occurred before you integrated the Adapty SDK into your app. A user purchased a monthly subscription on January 10th. The March 1st update of your application was the first to include the Adapty SDK.
If this setting is **on**, the report won't include the "subscription started" event from January, nor the "susbcription renewed" event from February. It **will** include the "subscription renewed" event from the 10th of March.
The reason why the user canceled a subscription.
Possible values:
**iOS & Android** — *voluntarily_cancelled*, *billing_error*, *refund*
**iOS only** — *price_increase*, *product_was_not_available*, *unknown*, *upgraded*
**Android only** — *new_subscription_replace*, *cancelled_by_developer*
| | **android_app_set_id** | An [AppSetId](https://developer.android.com/design-for-safety/privacy-sandbox/reference/adservices/appsetid/AppSetId) - unique, per-device, per developer-account user-resettable ID for non-monetizing advertising use cases. | | **android_id** | On Android 8.0 (API level 26) and higher versions of the platform, a 64-bit number (expressed as a hexadecimal string), unique to each combination of app-signing key, user, and device. For more details, see [Android developer documentation](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). | | **device** | The end-user-visible device model name. | | **currency** | The 3-letter currency code (ISO-4217) of the transaction. | | **store_country** | Profile Country determined by Apple/Google store. | | **attribution_source** | Attribution source. | | **attribution_network_user_id** | ID assigned to the user by attribution source. | | **attribution_status** | Can be organic, non_organic or unknown. | | **attribution_channel** | Marketing channel name. | | **attribution_campaign** | Marketing campaign name. | | **attribution_ad_group** | Attribution ad group. | | **attribution_ad_set** | Attribution ad set. | | **attribution_creative** | Attribution creative keyword. | | **attributes** | JSON of [custom user attributes](setting-user-attributes#custom-user-attributes). This will include any custom attributes you’ve set up to send from your mobile app. To send it, enable the **Send User Attributes** option in the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. | | **integration_ids** | All integration IDs associated with a profile. Dictionary. Example: {'mixpanel_user_id': 'mixpanelUserId-test', 'facebook_anonymous_id': 'facebookAnonymousId-test'} | ### Paywall visits | Column | Description | | :-------------------- | :----------------------------------------------------------------------------------------------------------- | | **profile_id** | Adapty user ID. | | **customer_user_id** | Developer user ID. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. | | **profile_country** | Profile Country determined by Apple/Google store. | | **install_date** | ISO 8601 date when the installation happened. | | **store** | Could be *app_store* or *play_store*. | | **paywall_showed_at** | The date when the paywall has been displayed to the customer. | | **developer_id** | Developer (SDK) ID of the paywall where the transaction originated. | | **ab_test_name** | Name of the A/B test where the transaction originated. | | **ab_test_revision** | Revision of the A/B test where the transaction originated. | | **paywall_name** | Name of the paywall where the transaction originated. | | **paywall_revision** | Revision of the paywall where the transaction originated. | ## Troubleshooting Adapty checks the validity of your access keys **before** it begins the upload. Even if only one of your Google Cloud Storage keys is invalid, Adapty **aborts the upload** and throws an error. To ensure uninterrupted uploads, replace your keys before they expire. If you update the key for **events**, don't forget to update the key for **paywall visits**, and vice versa. --- # File: webhook-event-types-and-fields --- --- title: "Webhook event types and fields" description: "" --- Adapty sends webhooks in response to subscription events. This section defines these event types and the data contained in each webhook. ## Webhook event types You can send all event types to your webhook or choose only some of them. You can consult our [Event flows](event-flows) to learn what kind of incoming data to expect and how to build your business logic around it. You can disable the event types you do not need when you [set up your Webhook integration](set-up-webhook-integration#configure-webhook-integration-in-the-adapty-dashboard). There, you can also replace Adapty default event IDs with your own if required. | Event name | Description | |:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscription_started | Triggered when a user activates a paid subscription without a trial period, meaning they are billed instantly. | | subscription_renewed | Occurs when a subscription is renewed and the user is charged. This event starts from the second billing, whether it's a trial or non-trial subscription. | | subscription_renewal_cancelled | A user has turned off subscription auto-renewal. The user retains access to premium features until the end of the paid subscription period. | | subscription_renewal_reactivated | Triggered when a user reactivates subscription auto-renewal. | | subscription_expired | Triggered when a subscription fully ends after being canceled. For instance, if a user cancels a subscription on December 12th but it remains active until December 31st, the event is recorded on December 31st when the subscription expires. | | subscription_paused | Occurs when a user activates [subscription pause](https://developer.android.com/google/play/billing/subs#pause) (Android only). | | subscription_deferred | Triggered when a subscription purchase is [deferred](https://adapty.io/glossary/subscription-purchase-deferral/), allowing users to delay payment while maintaining access to premium features. This feature is available through the Google Play Developer API and can be used for free trials or to accommodate users facing financial challenges. | | non_subscription_purchase | Any non-subscription purchase, such as lifetime access or consumable products like in-game coins. | | trial_started | Triggered when a user activates a trial subscription. | | trial_converted | Occurs when a trial ends and the user is billed (first purchase). For example, if a user has a trial until January 14th but is billed on January 7th, this event is recorded on January 7th. | | trial_renewal_cancelled | A user turned off subscription auto-renewal during the trial period. The user retains access to premium features until the trial ends but will not be billed or start a subscription. | | trial_renewal_reactivated | Occurs when a user reactivates subscription auto-renewal during the trial period. | | trial_expired | Triggered when a trial ends without converting to a subscription. | | entered_grace_period | Occurs when a payment attempt fails, and the user enters a grace period (if enabled). The user retains premium access during this time. | | billing_issue_detected | Triggered when a billing issue occurs during a charge attempt (e.g., insufficient card balance). | | subscription_refunded | Triggered when a subscription is refunded (e.g., by Apple Support). | | non_subscription_purchase_refunded | Triggered when a non-subscription purchase is refunded. | | access_level_updated | Occurs when a user's access level is updated. | ## Webhook event structure Adapty will send you only those events you've chosen in the **Events names** section of the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. Webhook events are serialized in JSON. The body of a `POST` request to your server will contain the serialized event wrapped into the structure below. All events follow the same structure, but their fields vary based on the event type, store, and your specific configuration. User attributes are the [custom user attributes](setting-user-attributes#custom-user-attributes) you set up, so they contain what you've configured. Attribution data fields are the same for all event types as well, however, the list of attributions will depend on which attribution sources you use in your mobile app. See below an example of an event: ```json title="Json" showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem", "idfv": "00000000-0000-0000-0000-000000000000", "idfa": "00000000-0000-0000-0000-000000000000", "advertising_id": "00000000-0000-0000-0000-000000000000", "profile_install_datetime": "2000-01-31T00:00:00.000000+0000", "user_agent": "ExampleUserAgent/1.0 (Device; OS Version) Browser/Engine", "email": "john.doe@company.com", "event_type": "subscription_started", "event_datetime": "2000-01-31T00:00:00.000000+0000", "event_properties": { "store": "play_store", "currency": "USD", "price_usd": 4.99, "profile_id": "00000000-0000-0000-0000-000000000000", "cohort_name": "All Users", "environment": "Production", "price_local": 4.99, "base_plan_id": "b1", "developer_id": "onboarding_placement", "ab_test_name": "onboarding_ab_test", "ab_test_revision": 1, "paywall_name": "UsedPaywall", "proceeds_usd": 4.2315, "variation_id": "00000000-0000-0000-0000-000000000000", "purchase_date": "2024-11-15T10:45:36.181000+0000", "store_country": "AR", "event_datetime": "2000-01-31T00:00:00.000000+0000", "proceeds_local": 4.2415, "tax_amount_usd": 0, "transaction_id": "0000000000000000", "net_revenue_usd": 4.2415, "profile_country": "AR", "paywall_revision": "1", "profile_event_id": "00000000-0000-0000-0000-000000000000", "tax_amount_local": 0, "net_revenue_local": 4.2415, "vendor_product_id": "onemonth_no_trial", "profile_ip_address": "10.10.1.1", "consecutive_payments": 1, "rate_after_first_year": false, "original_purchase_date": "2000-01-31T00:00:00.000000+0000", "original_transaction_id": "0000000000000000", "subscription_expires_at": "2000-01-31T00:00:00.000000+0000", "profile_has_access_level": true, "profile_total_revenue_usd": 4.99, "promotional_offer_id": null, "store_offer_category": null, "store_offer_discount_type": null }, "event_api_version": 1, "profiles_sharing_access_level": [{"profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem"}], "attributions": { "appsflyer": { "ad_set": "Keywords 1.12", "status": "non_organic", "channel": "Google Ads", "ad_group": null, "campaign": "Social media influencers - Rest of the world", "creative": null, "created_at": "2000-01-31T00:00:00.000000+0000" } }, "user_attributes": {"Favourite_color": "Violet", "Pet_name": "Fluffy"}, "integration_ids": {"firebase_app_instance_id": "val1", "branch_id": "val2", "one_signal_player_id": "val3"}, "play_store_purchase_token": { "product_id": "product_123", "purchase_token": "token_abc_123", "is_subscription": true } } ``` ### Event fields Event parameters are the same for all event types. | **Field** | **Type** | **Description** | |---|---|---| | **advertising_id** | UUID | Advertising ID (Android only). | | **attributions** | JSON | [Attribution data](webhook-event-types-and-fields#attribution-data). Included if **Send Attribution** is enabled in [Webhook settings](https://app.adapty.io/integrations/customwebhook). | | **customer_user_id** | String | User ID from your app (UUID, email, or other ID) if you set it in your app code when [identifying users](ios-quickstart-identify). If you don't identify users in the app code or this specific user is anonymous (not logged in), this field is `null`. | | **email** | String | User's email if you set it using the [`updateProfile`](setting-user-attributes) method in the Adapty SDK or when creating/updating profiles via the server-side API. If you don't pass the `email` value to the SDK or API method, this field is `null`. | | **event_api_version** | Integer | Adapty API version (current: `1`). | | **event_datetime** | ISO 8601 | Event timestamp in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format (e.g., `2020-07-10T15:00:00.000000+0000`). | | **event_properties** | JSON | [Event properties](webhook-event-types-and-fields#event-properties). | | **event_type** | String | Event name in Adapty format. See [Webhook event types](webhook-event-types-and-fields#webhook-event-types) for the full list. | | **idfa** | UUID | Advertising ID (Apple only). **IDFA** in the profile in the [Adapty Dashboard](https://app.adapty.io/profiles/users). It may be `null` if unavailable due to tracking restrictions, kids mode, or privacy settings. | | **idfv** | UUID | Identifier for Vendors (IDFV), unique per developer. **IDFV** in the profile in the [Adapty Dashboard](https://app.adapty.io/profiles/users). | | **integration_ids** | JSON | User integration IDs if you set them using the `setIntegrationIdentifier` method in the Adapty SDK or when creating/updating profiles via the server-side API. `null` if unavailable or integrations are disabled. | | **play_store_purchase_token** | JSON | [Play Store purchase token](webhook-event-types-and-fields#play-store-purchase-token), included if **Send Play Store purchase token** is enabled in [Webhook settings](https://app.adapty.io/integrations/customwebhook). | | **profile_id** | UUID | Profile ID automatically generated by Adapty for each profile. One Apple/Google ID can be associated with different profile IDs if you don't identify users or allow purchases before login. Learn [more about the way Adapty works with parent/inheritor profiles](profiles-crm#parent-and-inheritor-profiles). | | **profile_install_datetime** | ISO 8601 | Installation timestamp in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format (e.g., `2020-07-10T15:00:00.000000+0000`). | | **profiles_sharing_access_level** | JSON | List of users [sharing the access level](general#6-sharing-purchases-between-user-accounts) excluding the current user profile. If sharing access levels is enabled for your app, this list includes other profiles that have been used with the same Apple/Google ID.While custom attribute values in the mobile app code can be set as floats or strings, attributes received via the server-side API or historical import may come in different formats. The boolean and integer values will be converted to floats in this case.
| ### Attributions To send the attribution data, enable the **Send Attribution** option in the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. If you've enabled sending attribution data and if you have set up [attribution integrations](attribution-integration.md), the data below will be sent with the event for every source. The same attribution data is sent to all event types. ```json title="Json" showLineNumbers { "attributions": { "appsflyer": { "ad_set": "sample_ad_set_123", "status": "non_organic", "channel": "sample_channel", "ad_group": "sample_ad_group_456", "campaign": "sample_ios_campaign", "creative": "sample_creative_789", "created_at": "2000-01-31T00:00:00.000000+0000", "network_user_id": "0000000000000-0000000" } } } ``` | Field name | Field type | Description | | :------------------ | :------------ | :------------------------------------------------- | | **ad_set** | String | Attribution ad set. | | **status** | String | Can be `organic`, `non_organic,` or `unknown`. | | **channel** | String | Marketing channel name. | | **ad_group** | String | Attribution ad group. | | **campaign** | String | Marketing campaign name. | | **creative** | String | Attribution creative keyword. | | **created_at** | ISO 8601 date | Date and time of attribution record creation. | | **network_user_id** | String | ID assigned to the user by the attribution source. | ### Integration IDs The following integration IDs are used now in events: - `adjust_device_id` - `airbridge_device_id` - `amplitude_device_id` - `amplitude_user_id` - `appmetrica_device_id` - `appmetrica_profile_id` - `appsflyer_id` - `branch_id` - `facebook_anonymous_id` - `firebase_app_instance_id` - `mixpanel_user_id` - `pushwoosh_hwid` - `one_signal_player_id` - `one_signal_subscription_id` - `tenjin_analytics_installation_id` - `posthog_distinct_user_id` ### Play Store purchase token This field includes all the data needed to revalidate a purchase, if necessary. It is sent only if the **Send Play Store purchase token** option is enabled in the [Webhook integration settings](https://app.adapty.io/integrations/customwebhook). | Field | Type | Description | | :------------------ | :------ | :----------------------------------------------------------- | | **product_id** | String | The unique identifier of the product (SKU) purchased in the Play Store. | | **purchase_token** | String | A token generated by Google Play to uniquely identify this purchase transaction. | | **is_subscription** | Boolean | Indicates whether the purchased product is a subscription (`true`) or a one-time purchase (`false`). | ### Event properties Event properties can vary depending on the event type and even between events of the same type. For instance, an event originating from the App Store won’t include Android-specific properties like `base_plan_id`. The [Access Level Updated](webhook-event-types-and-fields#for-access-level-updated-event) event has distinct properties, so we’ve dedicated a separate section to it. Similarly, we’ve separated [Additional tax and revenue event properties](webhook-event-types-and-fields#additional-tax-and-revenue-event-properties), as they are specific to only certain event types. #### For most event types The event properties for most event types are consistent (except for **Access Level Updated** event, which is described in its own section). Below is a comprehensive table highlighting properties and indicating if they belong to specific events. | Field | Type | Description | |:------------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **ab_test_name** | String | Name of the [Adapty A/B test](ab-tests) where the transaction originated. | | **ab_test_revision** | Integer | Revision of the A/B test where the transaction originated. | | **base_plan_id** | String | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. | | **cancellation_reason** | String |Possible reasons for cancellation: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `cancelled_by_developer`, `new_subscription_replace`, `upgraded`, `unknown`, `adapty_revoked`.
Present in the following event types:
`subscription_cancelled`, `subscription_refunded`, and `trial_cancelled`. | | **cohort_name** | String | The name of the [audience](audience) that determined which paywall the user was shown. | | **consecutive_payments** | Integer | The number of periods, that a user is subscribed to without interruptions. Includes the current period. | | **currency** | String | Local currency. | | **developer_id** | String | The ID of the [placement](placements) where the transaction originated. | | **environment** | String | Possible values are `Sandbox` or `Production`. | | **event_datetime** | ISO 8601 date | The date and time of the event. The same as on the root level of the event. | | **original_purchase_date** | ISO 8601 date | For recurring subscriptions, the original purchase is the first transaction in the chain, its ID called original transaction ID links the chain of renewals; later transactions are extensions of it. The original purchase date is the date and time of this first transaction. | | **original_transaction_id** | String |For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are extensions of it.
If no extensions, `original_transaction_id` matches store_transaction_id.
| | **paywall_name** | String | Name of the paywall where the transaction originated. | | **paywall_revision** | String | Revision of the paywall where the transaction originated. The default value is 1. | | **price_local** | Float | Product price before Apple/Google cut in local currency. | | **price_usd** | Float | Product price before Apple/Google cut in USD. | | **profile_country** | String | Determined by Adapty, based on profile IP. | | **profile_event_id** | UUID | Unique event ID that can be used for deduplication. | | **profile_has_access_level** | Boolean | A boolean that indicates whether the profile has an active access level. | | **profile_id** | UUID | Adapty-generated profile ID. The same as on the root level of the event. | | **profile_ip_address** | String | Profile IP (can be IPv4 or IPv6, with IPv4 preferred when available). `null` if **Collect users' IP addresses** is disabled in the [app settings](https://app.adapty.io/settings/general). | | **profile_total_revenue_usd** | Float | Total revenue for the profile with refunds subtracted from the revenue. | | **promotional_offer_id** | String | The Adapty ID of the [promotional offer](offers) used. You set this ID when you create an offer in the dashboard. | | **purchase_date** | ISO 8601 date | The date and time of the product purchase. | | **rate_after_first_year** | Boolean | Boolean indicates that the subscription qualifies for a reduced commission rate (typically 15%) after one year of continuous renewal. Commission rates vary based on program eligibility and country. See [Store commission and taxes](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) for details. | | **store** | String | Store where the product was bought. Standard values: **app_store**, **play_store**, **stripe**, **paddle**.For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are extensions of it.
If no extensions, `original_transaction_id` matches store_transaction_id.
The transaction identifier of the original purchase. | | **paywall_name** | String | Name of the paywall where the transaction originated. | | **paywall_revision** | String | Revision of the paywall where the transaction originated. The default value is 1. | | **profile_country** | String | Determined by Adapty, based on profile IP. | | **profile_event_id** | UUID | Unique event ID that can be used for deduplication. | | **profile_has_access_level** | Boolean | Boolean indicating whether the profile has an active access level. | | **profile_id** | UUID | Adapty internal user profile ID. | | **profile_ip_address** | String | Profile IP address of the user. | | **profile_total_revenue_usd** | Float | Total revenue for the profile, refunds included. | | **purchase_date** | ISO 8601 date | The date and time of product purchase. | | **renewed_at** | ISO 8601 date | Date and time when the access will be renewed. | | **starts_at** | ISO 8601 date | Date and time when the access level starts. | | **store** | String | Store where the product was bought. Standard values: **app_store**, **play_store**, **stripe**, **paddle**.
1. **You set up your endpoint:** 1. Ensure your server can process Adapty requests with the **Content-Type** header set to `application/json`. 2. Configure your server to receive Adapty's verification request and respond with any `2xx` status and a JSON body. 3. [Handle subscription events](#subscription-events) once the connection is verified. 2. **You configure and enable the webhook integration** in the [Adapty Dashboard](#configure-webhook-integration-in-the-adapty-dashboard). You can also [map Adapty events to custom event names](#configure-webhook-integration-in-the-adapty-dashboard). We recommend testing in the **Sandbox environment** before switching to production. 3. **Adapty sends a verification request** to your server. 4. **Your server responds** with a `2XX` status and a JSON body. 5. **Once Adapty receives a valid response, it starts sending subscription events.** ## Set up your server to process Adapty requests Adapty will send to your webhook endpoint 2 types of requests: 1. [Verification request](#verification-request): the initial request to verify the connection is set up correctly. This request will not contain any event and will be sent the moment you click the **Save** button in the Webhook integration of the Adapty Dashboard. To confirm your endpoint successfully received the verification request, your endpoint should answer with the verification response. 2. [Subscription event](#subscription-events): A standard request Adapty server sends every time an event is created in it. Your server does not need to reply with any specific response. The only thing the Adapty server needs is to receive a standard 200-code HTTP response if it successfully receives the message. ### Verification request After you enable webhook integration in the Adapty Dashboard, Adapty will send a POST verification request containing an empty JSON object `{}` as a body. Set up your endpoint to have the **Content-Type header** as `application/json`, i.e. your server's endpoint should expect the incoming webhook request to have its payload formatted as JSON. Your server must reply with a 2xx status code and send any valid JSON response, for example: ```json title="Json" {} ``` Once Adapty receives the verification response in the correct format and with a 2xx status code, your Adapty webhook integration is fully configured. ### Subscription events Subscription events are sent with the **Content-Type** header set to `application/json` and contain event data in JSON format. For possible event types and request structures, see [Webhook event types and fields](webhook-event-types-and-fields). ## Configure webhook integration in the Adapty Dashboard Within Adapty, you can configure separate flows for production events and test events received from the Apple or Stripe sandbox environment or Google test account. For production events, use the **Production endpoint URL** field specifying the URL to which the callbacks will be sent. Additionally, configure the **Authorization header value for production endpoint** field - the header for your server to authenticate Adapty events. Note that we'll use the value specified in the **Authorization header value for production endpoint** field as the `Authorization` header exactly as provided, without any changes or additions. For test events, employ the **Sandbox endpoint URL** and **Authorization header value for sandbox endpoint** fields accordingly. To set up the webhook integration: 1. Open [Integrations -> Webhook](https://app.adapty.io/integrations/customwebhook) in your Adapty Dashboard.
2. Turn on the toggle to initiate the integration.
4. Fill out the integration fields:
| Field | Description |
| ------------------------------------------------------ | ------------------------------------------------------------ |
| **Production endpoint URL** | The URL Adapty uses to send HTTP POST requests for events in production. |
| **Authorization header value for production endpoint** | The header that your server will use to authenticate requests from Adapty in production. Note that we'll use the value specified in this field as the `Authorization` header exactly as provided, without any changes or additions.
Although not mandatory, it's strongly recommended for enhanced security.
| Additionally, for your testing needs in the sandbox environment, two other fields are available: | Testing field | Description | | --------------------------------------------------- | ------------------------------------------------------------ | | **Sandbox endpoint URL** | The URL Adapty uses to send HTTP POST requests for events in the sandbox environment. | | **Authorization header value for sandbox endpoint** |The header that your server will use to authenticate requests from Adapty during testing in the sandbox environment. Note that we'll use the value specified in this field as the `Authorization` header exactly as provided, without any changes or additions.
Although not mandatory, it's strongly recommended for enhanced security.
| 4. (optional) Pick the events you want to receive and map their names. Check out our [Event flows](event-flows) to see which events are triggered in different situations. If your event IDs differ from those used in Adapty, keep the IDs in your system as is and replace the default Adapty event IDs with yours in the **Events names** section of the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. The event ID can be any string; simply make sure the event ID in your webhook processing server coincides with the one you entered in the Adapty Dashboard. You cannot leave the event ID empty for enabled events.
5. Additional fields and options are not obligatory; use them as needed:
| Setting | Description |
| :--------------------------------- | :----------------------------------------------------------- |
| **Send Trial Price** | When enabled, Adapty will include the subscription price in the `price_local` and `price_usd` fields for the **Trial Started** event. |
| **Exclude Historical Events** | Opt to exclude events that occurred before the user installed the app with Adapty SDK. This prevents duplication of events and ensures accurate reporting. For instance, if a user activated a monthly subscription on January 10th and updated the app with Adapty SDK on March 6th, Adapty will omit events before March 6th and retain subsequent events. |
| **Send user attributes** | Enable this option to send user-specific attributes, such as language preferences. These attributes will appear in the `user_attributes` field. See [Event fields](webhook-event-types-and-fields#event-fields) for more information. |
| **Send attribution** | Turn on this option to include attribution information (e.g., AppsFlyer data) in the `attributions` field. Refer to the [Attribution data](webhook-event-types-and-fields#attribution-data) section for details. |
| **Send Play Store purchase token** | Turn on this option to receive the Play Store token required for purchase revalidation, if needed. Enabling it will add the `play_store_purchase_token` parameter to the event. For details on its content, refer to the [Play Store purchase token](webhook-event-types-and-fields#play-store-purchase-token) section. |
6. Remember to click the **Save** button to confirm the changes.
The moment you click the **Save** button, Adapty will send a verification request and will wait for your server verification response.
### Choose events to send and map event names
Choose the events you want to receive in your server by enabling the toggle next to it. If your event names differ from those used in Adapty and you need to keep your names as is, you can set up the mapping by replacing the default Adapty event names with your own in the **Events names** section of the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page.
The event name can be any string. You cannot leave the fields empty for enabled events. If you accidentally removed Adapty event name, you can always copy the name from the [Events to send to third-party integrations](events) topic.
## Handle webhook events
Webhooks are typically delivered within 5 to 60 seconds after the event occurs. Cancellation events, however, may take up to 2 hours to be delivered after a user cancels their subscription.
If your server's response status code is outside the 200-404 range, Adapty will retry sending the event up to 9 times over 24 hours with exponential backoff. We suggest you set up your webhook to do only basic validation of the event body from Adapty before responding. If your server can't process the event and you don't want Adapty to retry, use a status code within the 200-404 range. Also, handle any time-consuming tasks asynchronously and respond to Adapty quickly. If Adapty doesn't receive a response within 10 seconds, it will consider the attempt a failure and will retry.
---
# File: test-webhook
---
---
title: "Test webhook integration"
description: "Test webhook integrations in Adapty to automate subscription event tracking."
---
After you set up your integration, it's time to test it. You can test both your sandbox and production integration. We recommend starting with the sandbox one and validating the maximum on it:
- The events are sent and successfully delivered.
- You set up the options correctly for historical events, subscription price for the **Trial started** event, attribution, user attributes, and Google Play Store purchase token to be sent or not sent with an event.
- You mapped event names correctly and your server can process them.
## How to test
Before you start testing an integration, make sure you have already:
1. Set up the webhook integration as described in the [Set up webhook integration](set-up-webhook-integration) topic.
2. Set up the environment as described in the [Test in-app purchases in Apple App Store](test-purchases-in-sandbox.md) and [Test in-app purchases in Google Play Store](testing-on-android) topics. Make sure you built your test app in the sandbox environment rather than in the production one.
3. Make a purchase/start a trial/make a refund that will raise an event you've chosen to send to the webhook. For example, to get the **Subscription started** event, purchase a new subscription.
## Validation of the result
### Successful sending events result
In case of successful integration, an event will appear in the **Last sent events** section of the integration and will have the **Success** status.
### Unsuccessful sending events result
| Issue | Solution |
|-----|--------|
| The event did not appear | Your purchase did not occur and therefore the event was not created. Refer to the [Troubleshooting test purchases](troubleshooting-test-purchases) topic for the solution. |
| The event appeared and has the **Sending failed** status | We determine the deliverability based on HTTP status and consider everything **outside the 200-399 range** to be a fail.
To find more on the issue, hover over the **Sending failed** status of your unsuccessful event as shown below.
|
---
# File: handle-integration-errors
---
---
title: "Handle errors in integrations"
description: "Handle errors in integrations"
---
When using any attribution, messaging, or analytics integrations, you might encounter some common errors. See this guide for the troubleshooting cases.
## Data discrepancy
**Reason**: This might happen because not all your users use the app version that has the Adapty SDK.
**Solution**: To ensure the data consistency, you can force your users to update the app to a version with the Adapty SDK.
## Network errors
**Reason**: It’s most likely because there was no internet connection between the Adapty server and the integration server.
**Solution**: These issues usually don’t persist long and only affect a small number of events.
## Integration server failed to process the event
**Reason**: The integration is set up incorrectly.
**Solution**: See the article about the integration in our documentation. Ensure you have completed all the setup steps both in the Adapty dashboard, on the third-party tool side, and in your app code.
## Missing integration data
**Reason**: The profile is missing some integration-specific ID. This might happen when the integration is not set up properly in the app code.
**Solution**: See the article about the integration in our documentation. Ensure you have implemented methods from the code snippets in your app code, and these methods actually interact with your user profiles.
## Missing integration credentials
**Reason**: Some integration credentials are missing or incorrect.
**Solution**: Please check all the credentials for that integration on the Adapty dashboard. The issue might occur due to version or environment mismatch.
## The event has expired
**Reason**: The **Exclude historical events** option is enabled in the integration settings, and the event's creation date precedes the profile creation date in our system.
This can happen if a chain of transactions starting many years ago comes to Adapty through receipt validation for a profile created recently.
**Solution**: Make sure that it doesn’t happen for new events. If you want to send historical events to the integration, disable **Exclude historical events**.
## Disabled/unsupported event type
**Reason**: Either the event is not supported for this integration, or you have disabled it when setting up the integration. For example, `access_level_updated` events are not supported by most integrations.
**Solution**: Check in the integration documentation whether the integration support this event type. If it does, in the Adapty dashboard, ensure that this event type is enabled in the integration settings.
---
# File: test-purchases-in-sandbox
---
---
title: "Sandbox testing"
description: "Test purchases in the sandbox environment to ensure smooth transactions."
---
Once you've configured everything in the Adapty Dashboard and your mobile app, it's time to conduct in-app purchase testing.
**Note:** none of the test tools charge users when they test buying a product. The App Store doesn’t send emails for purchases or refunds made in the test environments.
:::info
To proceed with in-app purchases testing, make sure:
- You’ve completed the [quickstart](quickstart.md) guides on store integration, adding products, and the Adapty SDK integration.
- Your product is marked [**Ready to submit**](InvalidProductIdentifiers.md#step-2-check-products-step-3-check-products) in App Store Connect.
:::
## Sandbox testing
:::info
We recommend testing in-app purchases with a real device. While sandbox purchases can run on simulators, real devices are needed to fully test all flows, including payment dialogs and biometric prompts.
:::
You have two main ways to test in-app purchases:
- **Build in Xcode and run on a test device**: Convenient for developers and QA engineers.
- **Use a sandbox test account with TestFlight**: Suitable for anyone else.
Both these options are covered in the guide below.
### Step 1. Create Sandbox test account in App Store Connect
:::warning
Create a new Sandbox test account to ensure your purchase history is clean. If you reuse an existing account, any previously purchased products will remain available, and you won’t be able to test buying them again.
:::
You can create a new Sandbox test account in a few clicks:
1. Go to [**Users and Access** > **Sandbox** > **Test Accounts**](https://appstoreconnect.apple.com/access/users/sandbox) in App Store Connect and click **+**.
2. Enter the test user details. Make sure to define the **Country or Region** which you plan to test as it impacts the product availability for the region and purchase currency.
:::tip
- If you use Gmail or iCloud, you can reuse your existing email address with [plus sign subaddressing](https://www.wikihow.com/Use-Plus-Addressing-in-Gmail).
- You can use a random email address that doesn't even exist, but make sure to decline two-factor authentication (2FA) when you sign in on a test device later.
:::
3. Click **Create**.
### Step 2. Enable the Developer mode
:::note
Skip this step if the Developer mode is **already enabled** on your test device or if you **don't have a Mac device**.
:::
You’ll need a Mac with Xcode installed and your test device cable:
1. Open Xcode on your Mac. If you're going to test in-app purchases with TestFlight, you only need to have XCode installed; you don't need to have an app there.
2. Connect your test device to the Mac using the cable.
3. Go to **Settings > Privacy & Security > Developer Mode** on your test device and turn on **Developer Mode**.
### Step 3. Download the app from TestFlight
:::info
This step applies only if you're testing with TestFlight. If you’re building the app in Xcode, skip this step.
:::
For details on submitting your app to TestFlight, go to the [Apple documentation](https://developer.apple.com/documentation/StoreKit/testing-in-app-purchases-with-sandbox#Prepare-for-sandbox-testing).
Before downloading the TestFlight app, on your test device, make sure you are signed in with your production Apple Account. Then download the app you test from TestFlight.
:::danger
Do not open the app once downloaded. Just proceed with the next steps.
If accidentally opened, remove it from your test device and download it again. Otherwise, your purchase history may not be clear, and testing in-app purchases will lead to errors.
:::
### Step 4. Switch to Sandbox test account
4. Scroll down to the **Sandbox Apple Account** section and tap **Sign In**.
5. Sign in with your Sandbox Apple Account credentials.
### Step 5. Clear purchase history
If you've just created a new Sandbox test account and switched to it, you can skip this step as it only applies to repeated testing using the same Sandbox test account.
1. Go to **Settings > Developer > Sandbox Apple Account** on your test device.
2. Select **Manage** from the pop-up menu.
3. Go to **Account Settings** and tap **Clear Purchase History**.
:::danger
This step is required each time you repeat testing using the same Sandbox test account. In this case, you will also need to [sign out from your Sandbox test account](#step-4-switch-to-sandbox-test-account), then sign in again to clear the purchase history cache on the test device.
:::
### Step 6. Build in Xcode and run
:::info
This step applies only if you're testing with an Xcode build. If you're using TestFlight, skip this step.
:::
1. Connect your test device to your Mac.
2. Open Xcode.
3. Click **Run** in the toolbar or choose **Product > Run** to build and run the app on the connected device.
If the build is successful, Xcode will launch the app on your device and open a debugging session in the debug area.
Your app is now ready for testing on the device.
### Step 7. Make test purchase
Open the app and make your test purchase through a paywall.
Once done, go to the article on [validating test purchases](validate-test-purchases.md) to check your results.
### Step 8. Keep testing
Now, your testing environment is all set. If you want to test it once again, [clear the purchase history of the sandbox account](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-account-settings/).
## Testing issues
Below are common issues you may encounter when testing an app.
### TestFlight issues
You can't clear your purchase history **if you use TestFlight without the Sandbox test account**, which results in various issues and false testing outcomes.
If you accidentally forgot to [switch to the Sandbox test account](#step-4-switch-to-sandbox-test-account) and opened the app even once, TestFlight attributes your purchase history to your production Apple Account, which brings unexpected issues.
To fix it, follow these steps:
1. Remove the app from the test device.
2. Follow the steps for [Sandbox testing](#sandbox-testing).
:::note
It's important to not only reinstall the app, but also switch to the Sandbox test account, clear purchase history, and launch it using the Sandbox test account.
:::
### Shared access levels issues
If you repeat testing using the same Sandbox test account, you may face unexpected behavior with [shared access levels](profiles-crm.md#sharing-access-levels-between-profiles) for the test user.
To check if the user has an inherited access level, go to [Profiles & Segments](https://app.adapty.io/profiles/users) from the Adapty Dashboard and open the user's profile.
If the user has an inherited access level, follow these steps for accurate testing results:
1. Delete the parent profile.
2. Remove the app from the test device.
3. [Download the app from TestFlight](#step-3-download-the-app-from-testflight).
4. [Switch to Sandbox test account](#step-4-switch-to-sandbox-test-account).
5. [Clear purchase history](#step-5-clear-purchase-history).
6. [Open the app and make your test purchase](#step-6-make-test-purchase).
### Updating app in TestFlight
If the TestFlight app has been updated:
1. Remove the app from the test device.
2. [Download the app from TestFlight](#step-3-download-the-app-from-testflight).
3. [Switch to Sandbox test account](#step-4-switch-to-sandbox-test-account).
4. [Clear purchase history](#step-5-clear-purchase-history).
5. [Open the app and make your test purchase](#step-6-make-test-purchase).
### Authorization during the purchase process
If you have downloaded the TestFlight app and haven't [logged into the sandbox account from the device settings](#step-4-switch-to-sandbox-test-account), logging into it during the purchase process won't work. For purchase to succeed, you must log into your sandbox account from the device settings before attempting to make a purchase.
## Test subscriptions
When testing the app using the Sandbox test account, you can set up the subscription renewal rate for each tester in sandbox. Learn more about editing subscription renewal rates in the [official Apple documentation](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-account-settings).
By default, subscriptions renew up to 12 times before they stop, according to the following schedule:
| Subscription duration | 1 week | 1 month | 2 months | 3 months | 6 months | 1 year |
| :----------------------------- | :--------- | :--------- | :--------- | :--------- | :--------- | :--------- |
| Subscription renewal speed | 3 minutes | 5 minutes | 10 minutes | 15 minutes | 30 minutes | 1 hour |
| Length of Billing Retry | 10 minutes | 10 minutes | 10 minutes | 10 minutes | 10 minutes | 10 minutes |
| Length of Billing Grace Period | 3 minutes | 5 minutes | 5 minutes | 5 minutes | 5 minutes | 5 minutes |
:::note
Keep in mind that test transactions take up to 10 minutes to appear in the [Event feed](validate-test-purchases.md).
:::
## Test offers
Testing offers requires all user receipts to be deleted for eligibility to work correctly.
The most reliable way to test offers is using a completely new [Sandbox test account](#step-1-create-sandbox-test-account-in-app-store-connect). Repeated testing using the same Sandbox test account may cause unexpected behavior.
:::danger
If you repeat testing using the same Sandbox test account, make sure to [clear purchase history](#step-5-clear-purchase-history) to avoid eligibility-related issues.
:::
---
# File: local-sk-files
---
---
title: "StoreKit testing in Xcode"
description: "Test purchases in the sandbox environment to ensure smooth transactions."
---
StoreKit testing in Xcode allows you to test in-app purchases locally without setting up a sandbox account.
For this kind of testing, you need to:
1. [Create a product in Adapty](quickstart-products.md) and assign it a **App Store product ID**.
2. In Xcode, create a local [StoreKit configuration file](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode) and add a product to it. The product ID must be the same as **App Store product ID** in Adapty.
3. Add the StoreKit configuration file to your build schema and build the app. Launch it on the emulator or on your device.
## Should I use StoreKit testing in Xcode?
This way of testing is the most convenient if you are an app developer who wants to test the build on the go or to test different purchase scenarios using the Xcode features.
However, you must remember that this kind of testing is local, so no changes will appear on the Adapty dashboard. Before launching your app in the production environment, we recommend you test [working with profiles](ios-quickstart-identify.md) using the [sandbox environment](test-purchases-in-sandbox.md).
You **should** use StoreKit testing if you want to:
- Test the purchase logic
- Reproduce different purchase scenarios using the Xcode tools (e.g., cancelled payment or refund)
- Test using the emulator
You **shouldn't** use StoreKit testing if you want to:
- Test profile-related logic
- See whether your actions in the app appear in the Adapty dashboard
- Share your app with non-development teams for testing
## Step 1. Create a StoreKit configuration file
To create a StoreKit configuration file, in Xcode:
1. Click **File > New > File from template**. Then, select **StoreKit Configuration File** and click **Next**.
2. Give it a name. Then, depending on whether you have the products in App Store Connect already:
- Select **Sync this file with an app in App Store Connect**: To create a configuration file that will contain all your App Store Connect products, so you can test them locally.
- Don't select **Sync this file with an app in App Store Connect**: To create an empty configuration file where you will need to add products manually.
Click **Next**.
3. Don't add your app as a target. Just proceed. If you are working with products synced from App Store Connect, go to [Step 2](#step-2-add-the-configuration-file-to-the-build-scheme).
4. If your products are not synced from App Store Connect, click **+** at the bottom left and select a product type.
5. Enter a subscription group name and click **Next**.
6. Enter a reference name. In the **Product ID** field, enter the **App Store product ID** of your product in Adapty.
7. Configure pricing, offer, and other product settings in the configuration file. Or, add more products to it.
## Step 2. Add the configuration file to the build scheme
To build the app using this configuration file, you need to add it to a build scheme. The best practice is to separate testing and production schemes, so we suggest you create a new scheme for testing:
1. At the top, click your app name and select **New scheme**.
2. Enter a name for the scheme and click **OK**.
3. Click the app name again and select **Edit scheme**. IN the **StoreKit configuration**, select your local configuration file, so it will be used on build.
## Step 3. Build & test
Now, you can build the app and test in-app purchases without connecting to the App Store backend. You can purchase products and get access levels locally. These changes won't be reflected in the Adapty dashboard, but you can still test unlocking paid features locally.
[Read more](https://developer.apple.com/documentation/xcode/testing-in-app-purchases-with-storekit-transaction-manager-in-code) about other features available with StoreKit testing in Xcode.
---
# File: testing-on-android
---
---
title: "Test in-app purchases in Google Play Store"
description: "Test subscription purchases on Android using Adapty."
---
Testing in-app purchases (IAPs) in your Android app can be a crucial step before releasing your app to the public. Sandbox testing is a safe and efficient way to test IAPs without charging real money to your users. In this guide, we'll walk you through the process of sandbox testing IAPs on the Google Play Store for Android.
## Testing environment
To ensure optimal performance of your Android app, it's recommended that you test it on a real device instead of an emulator. While we have successfully tested on emulators, Google recommends using a real device.
If you do decide to use an emulator, make sure that it has Google Play installed. This will help ensure that your app is functioning properly.
## 1. Set up test account for app testing
To facilitate testing during later stages of development, you'll need to set up a test user for in-app purchase testing. This user will be the first account you log in with on your Android testing device.
Note that the primary account on an Android device can only be changed by performing a factory reset, which wipes all your data. Therefore, it's important to set up your test user account properly to avoid needing a factory reset.
:::important
The way you set up a test account will depend on the device you're using:
- If you have a dedicated testing device, create a **separate test account (a new Gmail account)**.
- If you don't have a dedicated testing device, you can use your own **personal account** and temporarily enable **License testing** for it.
- If you don't have an Android device at all, you can **create a separate test account and use it with an emulator**. However, this approach is not recommended since it doesn't let you catch all the possible real device issues.
:::
## 2. Enable License testing
Once you've set up a test user account, you'll need to configure licensing testing for your app. To do this, follow these steps:
1. In the Google Play Console sidebar, navigate to **Settings** and select **License testing** in the **Monetization** section.
2. Select an existing license testers list or create a new one.
3. Add the account you will be using for testing to the list and save changes. If your team members need to test the app as well, you can add their emails to the list, so access ig given to the whole group.
## 3. Create closed track and add test account to it
To start testing, you need to publish a signed version of your app to a closed track:
1. Open your app and select **Test and release > Testing > Closed testing** in the menu. There, click **Create track**.
2. Enter the closed testing track name and click **Create track**.
3. Add a testers list to the track.
4. From the **How testers join your test** section, copy the link and send it to the device logged into the test account. Open the link on your testing device to make the user a tester.
:::warning
Consider the following to ensure successful testing:
- Opening the opt-in URL marks your Play account for testing. If you don't complete this step, products will not load.
- Often developers will use a different application ID for their test builds. This will cause you problems since Google Play Services uses the application ID to find your in-app purchases.
- There are cases where a test user may be allowed to purchase consumables, but not subscriptions, if the test device does not have a PIN. This may manifest in a cryptic "Something went wrong" message. Make sure that the test device has a PIN, and that the device is logged into Google Play Store.
:::
## 4. Upload a signed APK to the closed track
Generate a signed APK or use Android App Bundle to upload a signed APK to the closed track you just created. You don't even need to roll out the release. Just upload the APK. You can find more information about this in [this](https://support.google.com/googleplay/android-developer/answer/9859348?visit_id=638929100639477968-3849460621&rd=1) support article.
:::important
If your app is new, you may need to make it available in your country or region. To do so, go to **Testing > Closed testing**, click on your test track, and go to **Countries/regions** to add the desired countries and regions.
:::
## 5. Test in-app purchases
After you've uploaded the APK, wait a few minutes for the release to process. Then, open your testing device and sign in with the email account you added to the Testers list. You can then test in-app purchases as you would on a production app.
## Read more
Read the following resources to learn more about testing in-app purchases in Android apps:
- [Renewal periods in sandbox](https://developer.android.com/google/play/billing/test#subs)
- [Testing one-time purchases](https://developer.android.com/google/play/billing/test#one-time)
---
# File: validate-test-purchases
---
---
title: "Validate test purchases"
description: "Validate test purchases in Adapty to ensure seamless transactions."
---
Before releasing your mobile app to production, it's crucial to test in-app purchases thoroughly. Please refer to our [Test in-app purchases in Apple App Store](test-purchases-in-sandbox.md) and [Test in-app purchases in Google Play Store](testing-on-android) topics for detailed guidance on testing. Once you begin testing, you need to verify the success of test purchases.
Every time you make a test purchase on your mobile device, view the corresponding transaction in the [**Event Feed**](https://app.adapty.io/event-feed) in the Adapty Dashboard. If the purchase does not appear in the **Event Feed**, it's not being tracked by Adapty.
## ✅ Test purchase is successful
If the test purchase is successful, its transaction event will be displayed in the **Event Feed**:
If transactions work as expected, proceed to the [Release checklist](release-checklist), and then proceed with the app release.
## ❌ Test purchase is not successful
If you observe no transaction event within 10 minutes or encounter an error in the mobile app, refer to the [ Troubleshooting](troubleshooting-test-purchases) and articles on error handling [for iOS](ios-sdk-error-handling), [for Android](android-sdk-error-handling), [for React Native](react-native-handle-errors.md), [for Flutter](error-handling-on-flutter-react-native-unity),[for Unity](unity-handle-errors.md), and [Kotlin Multiplatform](kmp-handle-errors.md) for potential solutions.
---
# File: troubleshooting-test-purchases
---
---
title: "Troubleshooting test purchases"
description: "Troubleshoot test purchases in Adapty and resolve common in-app transaction issues."
---
If you encounter transaction issues, please first make sure you have completed all the steps outlined in the [release checklist](release-checklist). If you've completed all the steps and still encounter issues, please follow the guidance provided below to resolve them:
## An error is returned in the mobile app
Refer to the error list for your platform: [for iOS](ios-sdk-error-handling), [for Android](android-sdk-error-handling), [for React Native](react-native-troubleshoot-purchases.md), [Flutter](error-handling-on-flutter-react-native-unity), and [Unity](unity-troubleshoot-purchases.md) and follow our recommendations to resolve the issue.
## Transaction is absent from the Event Feed although no error is returned in the mobile app
To resolve this issue, please check the following:
1. **For iOS**: Ensure you use a real device rather than a simulator.
2. Ensure that the `Bundle ID`/`Package name` of your app matches the one in the [**App settings**](https://app.adapty.io/settings/general).
3. Ensure the `PUBLIC_SDK_KEY` in your app matches the **Public SDK key** in the Adapty Dashboard: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general).
4. Ensure you are using a sandbox account – not a [local StoreKit configuration file](local-sk-files.md). If you have used a local StoreKit configuration file for testing before, ensure you are not using it in the current build.
## No event is present in my testing profile
This is normal behavior. A new user profile record is automatically created in Adapty when:
- A user runs your app for the first time
- A user logs out of your app
**Why this happens:** All transactions and events are tied to the profile that generated the first transaction. This keeps the entire transaction history (trials, purchases, renewals) linked to the same profile.
**What you'll see:** New profile records (called "non-original profiles") may appear without events but will retain access levels. You may see `access_level_updated` events. This is expected behavior.
**For testing:** To avoid multiple profiles, create a new test account (Sandbox Apple ID) each time you reinstall the app.
For more details, see [Profile record creation](profiles-crm#profile-record-creation).
Here is an example of a non-original profile. Notice the absence of events in the **User history** and the presence of an access level.
## Prices do not reflect the actual prices set in App Store Connect
In both Sandbox and TestFlight which uses the sandbox environment for in-app purchases, it's important to verify that the purchase flow functions correctly, rather than focusing on the accuracy of prices. It's worth noting that Apple's API can occasionally provide inaccurate data, particularly when different regions are configured for devices or accounts. And since the prices come directly from the Store and the Adapty backend does not affect purchase prices in any way, you can ignore any inaccuracy in prices during the testing of the purchases through Adapty.
Therefore, prioritize testing the purchase flow itself over the accuracy of prices to ensure it functions as intended.
## The transaction time in the Event Feed is incorrect
The **Event Feed** uses the time zone set in the **App Settings**. To align the time zone of events with your local time, adjust the **Reporting timezone** in [**App settings** -> **General** tab](https://app.adapty.io/settings/general).
## Paywalls and products take a long time to load
This issue can occur if your test account has a long transaction history. We highly recommend creating a new test account each time, as outlined in our [Create a Sandbox Test Account (Sandbox Apple ID) in App Store Connect](test-purchases-in-sandbox#step-1-create-a-sandbox-test-account--sandbox-apple-id-in-the-app-store-connect) section.
If you're unable to create a new account, you can clear the transaction history on your current account by following these steps on your iOS device:
1. Open **Settings** and tap **App Store**.
2. Tap your **Sandbox Apple ID**.
3. In the popup, select **Manage**.
4. On the **Account Settings** page, tap **Clear Purchase History**.
For more details, check out the [Apple Developer documentation](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox/#3894622).
---
# File: test-devices
---
---
title: "Testing devices"
description: "Learn how to manage test devices in Adapty for efficient app testing."
---
For testing purposes, you can assign your device as a test device, which disables caching and ensures that your changes are reflected immediately.
:::note
Testing devices are supported starting from specific SDK versions:
- iOS: 2.11.1
- Android: 2.11.3
- React Native: 2.11.1
Flutter and Unity support will be added later.
:::
## Mark your device as test
1. Open the [**App settings**](https://app.adapty.io/settings/general) in the Adapty Dashboard.
2. Scroll down to the **Test devices** section in the **General** tab.
3. Click the **Add test device** button.
4. In the **Add test device** window, enter:
| Field | Description |
|:-----------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Test device name** | Name of the test device(s) for your reference. |
| **ID used to identify this test device** | Choose the identifier type you plan to use to identify the test device(s). Follow our recommendations in the [Which identifier you should use](test-devices#which-identifier-you-should-use) section below to pick the best option. |
| **ID value** | Enter the value of the identifier. |
5. Remember to click **Add test device** button to save the changes.
## Which identifier you should use
To identify a device, you can use several identifiers. We recommend the following:
- **Customer User ID** for both iOS and Android devices if you A unique identifier set by you to identify your users in your system. This could be the user's email, your internal ID, or any other string. To use this option, you must
It is the best choice for identifying a test device, especially if you're using several devices for the same account. All the devices with this account will be considered test.
| | Adapty profile ID |A unique identifier for the [user profile](profiles-crm) in Adapty.
Use it if you cannot use Customer User ID, IDFA for iOS, or Advertising ID for Android. Note that the Adapty Profile ID can change if you reinstall the app or re-log in.
| #### How to obtain Customer User ID and Adapty profile ID Both identifiers can be obtained in the **Profile** details of the Adapty Dashboard: 1. Find the user's profile in the [**Adapty Profiles** -> **Event feed** tab](https://app.adapty.io/event-feed). :::note To find the exact profile, make a rare type of transaction. In this case, once the transaction appears in the [**Event Feed**](https://app.adapty.io/event-feed), you'll easily identify it. ::: 2. Copy **Customer user ID** and **Adapty ID** field values in the profile details:
### Apple identifiers
| Identifier | Usage |
|----------|-----|
| IDFA | The Identifier for Advertisers (IDFA) is a unique device identifier assigned by Apple to a user’s device.
It's ideal for iOS devices as it never changes on its own, although you can manually reset it.
**Note**: Since the rollout of iOS 14.5, advertisers must ask for user consent to access the IDFA. Ensure you are asking for consent in your app and you have provided it on your test device.
| | IDFV | The Identifier for Vendors (IDFV) is a unique alphanumeric identifier assigned by Apple to all apps on a single device from the same publisher/vendor. It can change if you reinstall or update your app. | #### How to obtain the IDFA Apple does not provide the IDFA by default. Obtain it from the profile attribution in the Adapty Dashboard: 1. Find the user's profile in the [**Adapty Profiles** -> **Event feed** tab](https://app.adapty.io/event-feed). :::note To find the exact profile, make a rare type of transaction. In this case, once the transaction appears in the [**Event Feed**](https://app.adapty.io/event-feed), you'll easily identify it. ::: 2. Open the profile details and copy the **IDFA** field value in the **Attributes** section:
Alternatively, you can [find the app on the App Store that will show your IDFA to you](https://www.apple.com/us/search/idfa?src=globalnav).
#### How to obtain the Identifier for vendors (IDFV)
To obtain the IDFV, ask your developer to request it using the following method for your app and display the received identifier to your logs or debug panel.
```swift showLineNumbers title="Swift"
UIDevice.current.identifierForVendor
```
### Google identifiers
| Identifier | Usage |
|----------|-----|
| Advertising ID | The Advertising ID is a unique device identifier assigned by Google to a user’s device.
It's ideal for Android devices as it never changes on its own, although you can manually reset it.
**Note**: To use it, turn off the **Opt out of Ads Personalization** in your **Ads** settings if you use Android 12 or higher.
| | Android ID | The Android ID is a unique identifier for each combination of app-signing key, user, and device. Available on Android 8.0 and higher versions. | #### How to obtain Advertising ID To find your device's advertising ID: 1. Open the **Settings** app on your Android device. 2. Click on **Google**. 3. Select **Ads** under **Services**. Your advertising ID will be listed at the bottom of the screen. #### How to obtain Android ID To obtain the Android ID, ask your developer to request the [ANDROID_ID](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID) using the following method for your app and display the received identifier in your logs or debug panel. ```kotlin showLineNumbers title="Kotlin/Java" android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID); ``` --- # File: release-checklist --- --- title: "Release checklist" description: "Follow Adapty’s release checklist to ensure a smooth app update process." --- We’re thrilled you’ve decided to use Adapty! We hope the implementation went well. This guide will walk you through the steps to ensure your app is ready for publishing in stores, and you can rest assured that the monetization flow works. ## Pre-flight essentials What you need before starting validation: - A real device with a sandbox account - Access to the Adapty Dashboard - Access to App Store Connect / Google Play Console :::note While sandbox purchases can run on simulators, real devices are needed to fully test all flows, including payment dialogs and biometric prompts. ::: ## Universal validations - [ ] **Store connection**: Ensure you have connected Adapty to App Store and/or Google Play: - [ ] [App Store](initial_ios) - [ ] [Google Play](initial-android) - [ ] **Subscription event delivery**: Confirm server notifications are set up: - [ ] [App Store server notifications](enable-app-store-server-notifications) - [ ] [Real-time developer notifications (RTDN)](enable-real-time-developer-notifications-rtdn) - [ ] **Profile identification**: Validate user identification logic and ensure purchases map to the correct profile: - [ ] [Check that the identification logic in your app code matches your use case](ios-quickstart-identify) - [ ] [Ensure you understand the parent/inheritor logic for sharing paid access between user profiles](sharing-paid-access-between-user-accounts) - [ ] **Offers**: If you have App Store promotional offers in the app, ensure that you have [added your In-app purchase key](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) both to the main field and to the **App Store promotional offers** section. - [ ] **Data collection**: Ensure privacy compliance: - [ ] If you need to comply with privacy regulations like GDPR or CCPA or your app is intended for kids, control whether you [enable IDFA and IP collection and sharing](sdk-installation-ios#data-policies). - [ ] If your app uses AppTrackingTransparency, ensure you're [sending the authorization status to Adapty](ios-deal-with-att). - [ ] **Privacy labels**: [Learn more](apple-app-privacy) about the data Adapty collects and which flags you'd need to set for a review. ## Purchase validations
2. Select **Product** > **Archive** from the top menu bar.
3. Wait for the archiving process to complete. The **Organizer** window opens automatically. Select your archive and click **Distribute App**.
4. Choose **App Store Connect** as the distribution method. Follow the prompts to complete the upload.
:::note
The upload may fail if required assets are missing, such as an app icon or launch screen. Check the Xcode error log for details.
:::
### Step 2. Check the build in App Store Connect
1. Go to [App Store Connect](https://appstoreconnect.apple.com) and open your app.
2. Scroll to the **Build** section. Ensure that the build you just uploaded appears there.
:::note
It may take a few minutes for the build to appear in App Store Connect after uploading.
:::
## Submit your app and products for review
After the build appears in the **Build** section, attach your in-app subscriptions and submit the app for Apple review.
### Step 1. Attach products to the submission
Each subscription must have the **Ready to Submit** status in App Store Connect before you can attach it. If a subscription is still in draft or missing metadata, it won't appear in the list.
1. On the same page, scroll to the **In-App Purchases and Subscriptions** section.
2. Click **Select in-app purchases or subscriptions**.
3. Select all products you want to include in this submission and click **Done**.
### Step 2. Submit for review
1. Complete all required fields on the page (description, screenshots, keywords, etc.).
2. In the **App Store Version Release** section, select whether you want to release your app automatically, manually, or on schedule after it is approved.
3. Click **Add for Review**, then click **Submit to App Review**.
Apple reviews apps within 1–2 days, though review times may vary.
## Verify your app in production
After Apple approves your app:
1. Make a real purchase (or wait for your first user to purchase).
2. Open the [**Event Feed**](https://app.adapty.io/event-feed) in the Adapty Dashboard and confirm that production transaction events appear.
3. Check that subscription events (renewals, cancellations) flow correctly — these depend on [App Store server notifications](enable-app-store-server-notifications) being configured.
If production events don't appear, verify your [App Store connection configuration](app-store-connection-configuration).
## Next steps
Your app is live. Start growing your subscription revenue:
- **[A/B testing](ab-tests)**: Experiment with different paywalls to find what converts best.
- **[Analytics](charts)**: Track subscription metrics like MRR, churn, and conversion.
- **Integrations**: Send subscription events to [analytics](analytics-integration) and [attribution](attribution-integration) platforms.
---
# File: general
---
---
title: "App settings"
description: "Explore general settings and configurations in Adapty for seamless use."
---
You can navigate to the General tab of the App Settings page to manage your app's behavior, appearance, and revenue sharing. Here, you can customize your app's name and icon, manage your Adapty SDK and API keys, set your Small Business Program status, and choose the timezone for your app's analytics and charts.
## 1. App details
Choose a unique name and icon that represent your app in the Adapty interface. Please note that the app name and icon will not affect the app's name and icon in the App Store or Google Play. Also, make sure to select an appropriate App Category that accurately reflects your app's purpose and content. This will help users discover your app and ensure it appears in the appropriate app store categories.
## 2\. Member of Small Business Program and Reduced Service Fee
If your organization is enrolled in Apple's [Small Business Program](app-store-small-business-program) or Google's [Reduced Service Fee program](google-reduced-service-fee), your apps are subject to a reduced store commission.
Notify Adapty if your app is enrolled in a reduced commission program. To ensure correct calculations, specify these programs' status in your the "Reduced Store Fee" section.
The reduced fee setting only applies to future transactions. Change your status **before** it goes into effect, and Adapty will adjust the commission rate.
:::warning
* If you extend your paticipation in a reduced fee program, **add an extra eligibility period**.
* If you lose program membership, **change the expiration date** of your current eligibility period.
:::
The following articles explore this subject in depth:
* [App Store Small Business Program](app-store-small-business-program)
* [Google Reduced Service Fee](google-reduced-service-fee)
## 3\. Reporting timezone
Choose the timezone that corresponds to the location where you're based, or where your app's analytics and charts are most relevant. We recommend using the same timezone as your App Store Connect or Google Play Console account to ensure consistency. Please note that this timezone setting does not affect third-party integrations in the Adapty system, which use the UTC timezone.
You can access the timezone settings in the Reported timezone section of the General Tab on the App Settings page. You can also choose to set the same timezone for all the apps in your Adapty account by checking the corresponding box.
## 4\. Installs definition for analytics
Choose what is defined as a new install event in analytics:
| Base | Description |
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| New device_ids | (Recommended) Each installation of the app from the store on a device is counted as a new install. This includes both first-time installs and reinstalls.
Installs are counted per device ID and are not affected by user authentication. Creating a profile (on SDK activation or logout), logging in, or upgrading the app does not generate additional install events.
For example, if the same app is installed on 5 different devices, you will see 5 installs in analytics.
| | New customer_user_ids |This option is intended for apps that
For logged-in users, only the first installation associated with a customer user ID is counted as an install. Installations on additional devices are not counted as new installs.
Anonymous users (users who have not logged in) are not counted in analytics.
Reinstalling the app or logging in again does not create additional installs.
App stores and attribution platforms (such as App Store Connect, Google Play Console, and AppsFlyer) use a device-based approach to counting installs. If you count installs by customer user IDs in Adapty, install numbers may differ from these external services.
⚠️ If you do not identify users in Adapty, no installs will be counted with this option enabled.
| | New profiles in Adapty | (Legacy) Every app installation, reinstallation and anonymous profiles created during logouts are counted as new installs. | Keep in mind that this option only affects the [**Analytics**](https://app.adapty.io/analytics) page and does not impact the [**Overview**](https://app.adapty.io/overview) page, where you can configure the view separately. ## 5. App Store price increase logic To maintain accurate data and avoid discrepancies between Adapty analytics and App Store Connect results, it is important to select the appropriate option when adjusting configurations related to price increases in App Store Connect. So you can choose the logic that will be applied to subscription price increases in Adapty:
- **Subscription price for existing users is preserved:** By selecting this option, the current price will be retained for your existing subscribers, even if you make changes to the price in the App Store Connect. This means that existing subscribers will continue to be billed at their original subscription price.
- **When the subscription price is changed in App Store Connect, it changes for existing subscribers:** If you choose this option, any price changes made in the App Store Connect will be applied to your existing subscribers as well. This means that existing subscribers will be charged the new price reflecting the updated pricing set in the App Store Connect.
:::warning
It is important to consider that the selected option not only affects analytics in Adapty but also impacts integrations and overall transaction handling behavior.
:::
Please ensure that you select the designated option that aligns with your desired approach to handling subscription prices for existing subscribers. This will help maintain accurate data and synchronization between Adapty analytics and the results obtained from the App Store Connect.
## 6. Sharing paid access between user accounts
:::link
Main article: [Sharing paid access between user accounts](sharing-paid-access-between-user-accounts)
:::
The **Sharing paid access between user accounts** setting determines what Adapty does when more than one [user profile](identifying-users) attempts to access the same purchase. You can specify a separate access sharing setting for the [sandbox environment](test-purchases-in-sandbox).
---
no_index: true
---
**Enabled (default)**
Identified users (those with a [Customer User ID](identifying-users#setting-customer-user-id-on-configuration)) can share the same [access level](https://adapty.io/docs/access-level) provided by Adapty if their device is signed in to the same Apple/Google ID. This is useful when a user reinstalls the app and logs in with a different email — they’ll still have access to their previous purchase. With this option, multiple identified users can share the same access level.
Even though the access level is shared, all past and future transactions are logged as events in the original Customer User ID to maintain consistent analytics and keep a complete transaction history — including trial periods, subscription purchases, renewals, and more, linked to the same profile.
**Transfer access to new user**
Identified users can keep accessing the [access level](access-level) provided by Adapty, even if they log in with a different [Customer User ID](identifying-users#setting-customer-user-id-on-configuration) or reinstall the app, as long as the device is signed in to the same Apple/Google ID.
Unlike the previous option, Adapty transfers the purchase between identified users. This ensures that the purchased content is available, but only one user can have access at a time. For example, if UserA buys a subscription and UserB logs in on the same device and restores transactions, UserB will gain access to the subscription, and it will be revoked from UserA.
If one of the users (either the new or the old one) is not identified, the access level will still be shared between those profiles in Adapty.
Although the access level is transferred, all past and future transactions are logged as events in the original Customer User ID to maintain consistent analytics and keep a complete transaction history — including trial periods, subscription purchases, renewals, and more, linked to the same profile.
After switching to **Transfer access to new user**, access levels won’t be transferred between profiles immediately. The transfer process for each specific access level is triggered only when Adapty receives an event from the store, such as subscription renewal, restore, or when validating a transaction.
**Disabled**
The first identified user profile to get an access level will retain it forever. This is the best option if your business logic requires that purchases be tied to a single Customer User ID.
Note that access levels are still shared between anonymous users.
You can "untie" a purchase by [deleting the owner’s user profile](ss-delete-profile). After deletion, the access level becomes available to the first user profile that claims it, whether anonymous or identified.
Disabling sharing only affects new users. Subscriptions already shared between users will continue to be shared even after this option is disabled.
:::warning
Apple and Google require in-app purchases to be shared or transferred between users because they rely on the Apple/Google ID to associate the purchase with. Without sharing, restoring purchases might not work upon subsequent reinstalls.
Disabling sharing may prevent users from regaining access after logging in.
We recommend disabling sharing only if your users **are required to log in** before they make a purchase. Otherwise, an identified user could buy a subscription, log into another account, and lose access permanently.
:::
### Which setting should I choose?
| My app... | Option to choose |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| Does not have a login system and only uses Adapty’s anonymous profile IDs. | Use the default option, as access levels are always shared between anonymous profile IDs for all three options. |
| Has an optional login system and allows customers to make purchases before creating an account. | Choose **Transfer access to new user** to ensure that customers who purchase without an account can still restore their transactions later. |
| Requires customers to create an account before purchasing but allows purchases to be linked to multiple Customer User IDs. | Choose **Transfer access to new user** to ensure that only one Customer User ID has access at a time, while still allowing users to log in with a different Customer User ID without losing their paid access. |
| Requires customers to create an account before purchasing, with strict rules that tie purchases to a single Customer User ID. | Choose **Disabled** to ensure that transactions are never transferred between accounts. |
## 7. SDK and API keys
Use a Public SDK key to integrate Adapty SDKs into your app, and a Secret Key to access Adapty's Server API. You can generate new keys or revoke existing ones as needed.
## 8. Test devices
Specify the devices to be used for testing to ensure they get instant updates for paywall or placement changes, bypassing any caching delays. For more information, see [Testing devices](test-devices).
## 9. Cross-placement variation stickiness
Define how long after a test completion a user is still served with the variants in the test. This affects analytics accuracy and user experience — as serving a user with a different offer than what they have seen before might influence their decision to buy.
The maximum and default stickiness period is 90 days.
:::warning
Consider the following:
- Changing this setting will affect all the users who previously received a variation. They will instantly qualify for a new paywall when they see a placement, which can spoil the results of your running A/B tests.
- If the stickiness period is over for a user, they can be served with a new paywall or A/B test. However, even then, they won't be able to be a part of any other cross-placement test ever.
:::
## 10. Delete the app
If you no longer need an app, you can delete it from Adapty.
:::warning
Please be aware that this action is irreversible, and you won't be able to restore the app or its data.
:::
---
# File: ios-settings
---
---
title: "Apple App Store credentials"
description: "Configure iOS settings in Adapty for seamless subscription management."
---
To configure the App Store credentials and ensure optimal functionality of the Adapty iOS SDK, navigate to the [iOS SDK](https://app.adapty.io/settings/ios-sdk) tab within the App Settings page of the Adapty Dashboard. Then, configure the following parameters:
| Field | Description |
|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Bundle ID** | Your [app bundle ID](app-store-connection-configuration#step-1-provide-bundle-id). |
| **In-app purchase API (StoreKit 2)** | [Keys](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) to enable secure authentication and validation of in-app purchase transaction history requests. |
| **App Store Server Notifications** | URL that is used to enable [server2server notifications](enable-app-store-server-notifications) from the App Store to monitor and respond to users' subscription status changes |
| **App Store Promotional Offers** | Subscription keys for creating [Promotional offers](generate-in-app-purchase-key) in Adapty for specific products. |
| **Apple app ID** | Your app ID from the App Store. To find it, open your app page in App Store Connect, open the **App Information** page from the left menu and copy **Apple ID**. |
| **App Store Connect shared secret (LEGACY)** | **Legacy key for Adapty SDK prior to v.2.9.0**
[A key](app-store-connection-configuration#step-4-enter-app-store-shared-secret) for receipts validation and preventing fraud in your app.
| --- # File: google-play-store-connection-configuration --- --- title: "Configure Google Play Store integration" description: "Configure Google Play Store connection in Adapty for smooth in-app purchase handling." --- This section outlines the integration process for your mobile app sold via Google Play with Adapty. You'll need to input your app's configuration data from the Play Store into the Adapty Dashboard. This step is crucial for validating purchases and receiving subscription updates from the Play Store within Adapty. You can complete this process during the initial onboarding or make changes later in the **App Settings** of the Adapty Dashboard. :::danger Configuration change is only acceptable until you release your mobile app with integrated Adapty paywalls. The change after the release will break the integration and the paywalls will stop showing in your mobile app. ::: ## Step 1. Provide Package name The Package name is the unique identifier of your app in the Google Play Store. This is required for the basic functionality of Adapty, such as subscription processing. 1. Open the [Google Play Developer Console](https://play.google.com/console/u/0/developers). 2. Select the app whose ID you need. The **Dashboard** window opens.
3. Find the product ID under the application name and copy it.
4. Open the [**App settings**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu.
5. In the **Android SDK** tab of the **App settings** window, paste the copied **Package name**.
## Step 2. Upload the account key file
1. Upload the service account private key file in JSON format that you have created at the [Create service account key file](create-service-account) step into the **Service account key file** area.
Don't forget to click the **Save** button to confirm the changes.
**What's next**
- [Enable Real-time developer notifications (RTDN) in the Google Play Console](enable-real-time-developer-notifications-rtdn)
---
# File: enable-real-time-developer-notifications-rtdn
---
---
title: "Enable Real-time developer notifications (RTDN) in Google Play Console"
description: "Stay informed about critical events and maintain data accuracy by enabling Real-time Developer Notifications (RTDN) in the Google Play Console for Adapty. Learn how to set up RTDN to receive instant updates about refunds and other important events from the Play Store"
---
Setting up real-time developer notifications (RTDN) is crucial for ensuring data accuracy as it enables you to receive updates instantly from the Play Store, including information on refunds and other events.
## Enable notifications
1. Ensure you have **Google Cloud Pub/Sub** enabled. Open [this link](https://console.cloud.google.com/flows/enableapi?apiid=pubsub) and select your app project. If you haven't enabled **Google Cloud Pub/Sub**, you must do it here.
2. Go to [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu and copy the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title.
:::note If the contents of the **Enable Pub/Sub API** field have a wrong format (the correct format starts with `projects/...`), refer to the [Fixing incorrect format in Enable Pub/Sub API field](enable-real-time-developer-notifications-rtdn#fixing-incorrect-format-in-enable-pubsub-api-field) section for help. ::: 3. Open the [Google Play Console](https://play.google.com/console/), choose your app, and go to **Monetize with Play** -> **Monetization setup**. In the **Google Play Billing** section, select the **Enable real-time notifications** check-box. 4. Paste the contents of the **Enable Pub/Sub API** field you've copied in the Adapty **App Settings** into the **Topic name** field. 5. Click **Save changes** in the Google Play Console.
## Test notifications
To check whether you have successfully subscribed to real-time developer notifications:
1. Save changes in the Google Play Console settings.
2. Under the **Topic name** in Google Play Console, click **Send test notification**.
3. Go to the [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) in Adapty. If a test notification has been sent, you'll see its status above the topic name.
## Fixing incorrect format in Enable Pub/Sub API field
If the contents of the **Enable Pub/Sub API** field are in the wrong format (the correct format starts with `projects/...`), follow these steps to troubleshoot and resolve the issue:
### 1. Verify API Enablement and Permissions
Carefully ensure that all required APIs are enabled, and permissions are correctly granted to the service account. Even if you've already completed these steps, it’s important to go through them again to make sure no sub-step was missed. Repeat the steps in the following sections:
1. [Enable Developer APIs in Google Play Console](enabling-of-devepoler-api)
2. [Create service account in the Google Cloud Console](create-service-account)
3. [Grant permissions to service account in the Google Play Console](grant-permissions-to-service-account)
4. [Generate service account key file in the Google Play Console](create-service-account-key-file)
5. [Configure Google Play Store integration](google-play-store-connection-configuration)
### 2. Adjust Domain Policies
Change the **Domain restricted contacts** and **Domain restricted sharing** policies:
1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select the project where you created the service account to manage your app.
2. In the **Quick Access** section, choose **IAM & Admin**.
3. In the left pane, choose **Organization Policies**.
4. Find the **Domain restricted contacts** policy.
5. Click the ellipsis button in the **Actions** column and choose **Edit policy**.
6. In the policy editing window:
1. Under **Policy source**, select the **Override parent's policy** radio-button.
2. Under **Policy enforcement**, select the **Replace** radio button.
3. Under **Rules**, click the **ADD A RULE** button.
4. Under **New rule** -> **Policy values**, choose **Allow All**.
5. Click **SET POLICY**.
7. Repeat steps 4-6 for the **Domain restricted sharing** policy.
Finally, recreate the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title. The field will now have the correct format.
Make sure to switch the **Policy source** back to **Inherit parent's policy** for the updated policies once you've successfully enabled Real-time Developer Notifications (RTDN).
## Raw events forwarding
Sometimes, you might still want to receive raw S2S events from Google. To continue receiving them while using Adapty, just add your endpoint to the **URL for forwarding raw Google events** field, and we'll send raw events as-is from Google.
---
**What's next**
Set up the Adapty SDK for:
- [Android](sdk-installation-android)
- [React Native](sdk-installation-reactnative)
- [Flutter](sdk-installation-flutter)
- [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md)
- [Unity](sdk-installation-unity)
---
# File: apple-search-ads
---
---
title: "Apple Ads"
description: "Integrate Apple Ads with Adapty to optimize subscription conversions."
---
:::important
The Apple Ads integration in **App settings** is used only for basic analytics and for SplitMetrics Acquire and Asapty integrations.
[Apple Ads Manager](adapty-ads-manager.md) uses a separate connection. Connect your Apple Ads account in the [Apple Ads Manager settings](adapty-ads-manager-get-started.md).
:::
Adapty can help you get attribution data from Apple Ads and analyze your metrics with campaign and keyword segmentation. Adapty collects the attribution data for Apple Ads automatically through its SDK and AdServices Framework.
Once you've set up the Apple Ads integration, Adapty will start receiving attribution data from Apple Ads. You can easily access and view this data on the profiles page.
## Set up integration
### Connect Adapty to the AdServices framework
Apple Ads via [AdServices](https://developer.apple.com/documentation/adservices) does require some configuration in Adapty Dashboard, and you will also need to enable it on the app side. To set up Apple Ads using the AdServices framework through Adapty, follow these steps:
#### Step 1: Configure Info.plist
Add `AdaptyAppleSearchAdsAttributionCollectionEnabled` to the app’s `Info.plist` file and set it to `YES` (boolean value).
#### Step 2: Obtain public key
In the Adapty Dashboard, navigate to [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads)
Locate the pre-generated public key (Adapty provides a key pair for you) and copy it.
:::note
If you're using an alternative service or your own solution for Apple Ads attribution, you can upload your own private key.
:::
#### Step 3: Configure user management on Apple Ads
In your [Apple Ads account](https://ads.apple.com/app-store) go to **Settings > User Management** page. In order for Adapty to fetch attribution data you need to invite another Apple ID account and grant it API Account Manager access. You can use any account you have access to or create a new one just for this purpose. The important thing is that you must be able to log into Apple Ads using this Apple ID.
#### Step 4: Generate API credentials
As a next step, log in to the newly added account in Apple Ads. Navigate to Settings -> API in the Apple Ads interface. Paste the previously copied public key into the designated field. Generate new API credentials.
#### Step 5: Configure Adapty with Apple Ads credentials
Copy the Client ID, Team ID, and Key ID fields from the Apple Ads settings. In the Adapty Dashboard, paste these credentials into the corresponding fields.
### Connect your app to the AdServices network
Once you complete [the AdServices framework setup](#connect-the-adservices-framework), Adapty automatically starts collecting Apple Search Ad attribution data. You don't need to add any SDK code.
For iOS applications, this attribution data will **always** take priority over data from other sources. If this behaviour is unwanted, *disable* ASA attribution with the instructions below.
## Disable integration
To turn Apple Search Ads attribution off, open the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads), and toggle the **Receive Apple Search Ads attribution** switch.
:::warning
Please note that disabling this will completely stop the reception of ASA analytics. As a result, ASA will no longer be used in analytics or sent to integrations. Additionally, SplitMetrics Acquire and Asapty will cease to function, as they rely on ASA attribution to operate correctly.
The attribution received before this change will not be affected.
:::
## Uploading your own keys
:::note
Optional
These steps are not required for Apple Ads attribution, only for working with other services like Asapty or your own solution.
:::
You can use your own public-private key pair if you are using other services or own solution for ASA attribution.
### Step 1
Generate private key in Terminal
```text showLineNumbers title="Text"
openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
```
Upload it in Adapty Settings -> Apple Ads (Upload private key button)
### Step 2
Generate public key in Terminal
```text showLineNumbers title="Text"
openssl ec -in private-key.pem -pubout -out public-key.pem
```
You can use this public key in your Apple Ads settings of account with API Account Manager role. So you can use generated Client ID, Team ID, and Key ID values for Adapty and other services.
---
# File: account
---
---
title: "Account details & Billing"
description: "Manage your Adapty account and optimize settings for better subscription tracking."
---
The **Account** page lets you manage your profile, team members, and billing.
The page has three tabs:
- [General](#general-settings)
- [Subscription & Billing](#billing-info)
- [Members](#members)
To access your account settings, click **Account** at the top right or go to [app.adapty.io/account](https://app.adapty.io/account).
## General settings
The General tab contains your profile, account settings, display preferences, and report configuration.
- **Profile**: Enter your first name, last name, and company name.
- **Account settings**: View your registered email address and change your password.
- **Date & Time formats**: Choose how dates and times display throughout Adapty:
- **American format**: January 31, 2022 and 12-hour time (AM/PM)
- **European format**: 31 January, 2022 and 24-hour time (16:00)
- **Email reports**: Set up daily, weekly, or monthly reports for one or all of your apps. Receive summarized reports for all apps at once, or get a granular report for each selected app.
## Subscription & Billing
The **Subscription & Billing** tab lets you manage your payment information and feature access:
- Add or update payment details
- Review billing information
- Purchase additional paid features
Learn more about [features and pricing](https://adapty.io/pricing).
## Members
You can manage your team members in your account settings. To add team members, invite them by their email and assign them a role.
Read more about managing team members and their access rights [here](members-settings).
---
# File: members-settings
---
---
title: "Members"
description: "Manage member settings and permissions in Adapty’s dashboard."
---
:::note
This page is about Adapty dashboard members
If you want to give different access levels to users of your app, check [Access Level](access-level).
:::
The Adapty dashboard members system allows you to grant different levels of access to Adapty and specify applications for each member.
### Roles
The following roles are available for members in the Adapty dashboard:
| Role | Access to Billing | Add new members | Change anything | Access to all sections |
|-------------|-------------------|-----------------|-----------------|------------------------|
| Owner | ✅ | ✅ | ✅ | ✅ |
| Admin | ❌ | ✅ | ✅ | ✅ |
| Developer | ❌ | ❌ | ✅ | ❌ |
| Viewer | ❌ | ❌ | ❌ | ✅ |
| Support | ❌ | ❌ | ❌ | ❌ |
| ASA manager | ❌ | ❌ | ❌ | ❌ |
- **Owner:** The Owner is the original creator of the Adapty account and holds the highest level of access and control. Owners have complete access to Adapty billing, allowing them to manage payment information and subscription plans. Additionally, only Owners and Admins can specify application access for new members. There can be only one Owner for each Adapty account.
- **Admin:** Members with the Admin role have full access to the chosen applications. They can perform various management tasks, including creating and modifying paywalls, conducting A/B tests, analyzing analytics, and managing members within those applications.
- **Developer**: Members with the Developer role have full access to all the entities, except for analytics and account members. They can't access any billing settings. This role is intended for those who set up paywalls, A/B tests, and other entities and integrate Adapty into your app, but shouldn't see any financial data.
- **Viewer:** Members with the Viewer role have read-only access to the chosen applications. They can view information but cannot create or modify paywalls, A/B tests, and other features, invite new users, create new apps, and change the app settings.
- **Support:** Members with the Support role have access only to user profiles in chosen applications. However, they cannot perform actions like adding new members or accessing any other sections of Adapty. This role is particularly suitable for support teams or individuals who need to assist customers with subscription-related inquiries or troubleshooting.
- **ASA manager**: Members with the ASA manager role have access only to the [Apple Ads Manager](adapty-ads-manager) dashboard.
### Add a member
In Adapty, you can invite as many team members as you need free of charge.
:::note
You can only invite email addresses not yet registered in Adapty. If your colleague has a standalone account, invite a different email address or contact Adapty support to delete their existing account.
:::
To add a team member:
1. Click **Account** at the top right and open the **Members** tab.
2. Click **Invite member**.
3. Enter the member's email address.
4. Select a [role](#roles) from the list.
5. Select apps to provide access to.
6. (Optional) Enable **Always allow access to new apps** to automatically grant access to future apps.
7. Click **Save**.
### Transfer account ownership
If you need to transfer the whole **account ownership**, contact our support team at [support@adapty.io](mailto:support@adapty.io).
If you need to transfer the **app ownership**, read the [dedicated guide](transfer-apps) for more information.
---
# File: set-up-app-store-connect
---
---
title: "Set up App Store Connect"
description: "A first-time developer's guide to enrolling in the Apple Developer Program and setting up App Store Connect for in-app purchases."
---
If you're **building your first iOS app**, you must set up your Apple Developer account and App Store Connect before you integrate Adapty.
:::note
If you already have an Apple Developer account and an app registered in App Store Connect, you can skip this guide and go straight to [Initial integration with the App Store](initial_ios).
:::
## Step 1. Enroll in Apple Developer Program
To distribute apps on the App Store and sell in-app purchases, you must join the [Apple Developer Program](https://developer.apple.com/programs/).
### Choose enrollment type
Apple offers two enrollment types:
| | Individual | Organization |
|-----------------------------|--------------------|------------------------------|
| **Who it's for** | Solo developers | Companies, teams, nonprofits |
| **Requires D-U-N-S Number** | No | Yes |
| **Apps published under** | Your personal name | Your organization's name |
| **Team management** | Not available | Available |
:::tip
If you're enrolling as an organization, you need a **D-U-N-S Number** — a unique nine-digit business identifier from Dun & Bradstreet. You can [check if your organization already has one](https://developer.apple.com/enroll/duns-lookup/) or request a new one — the link is at the bottom of the lookup page. A D-U-N-S Number may take up to 5 business days to receive.
:::
### Enroll
1. Go to the [Apple Developer Program enrollment page](https://developer.apple.com/programs/enroll/).
2. Sign in with your Apple ID. If you don't have one, create it first.
3. Follow the steps for your enrollment type (individual or organization).
4. Pay the annual fee.
After Apple processes your enrollment, you receive access to [App Store Connect](https://appstoreconnect.apple.com). Enrollment typically takes up to 48 hours. For organizations, it may take longer if D-U-N-S verification is required.
## Step 2. Set up your app in App Store Connect
Before you can sell in-app purchases, complete the initial setup in App Store Connect. This includes signing agreements, adding payment details, and registering your app.
### Sign the Paid Applications Agreement
Apple requires you to sign the Paid Applications Agreement before you can sell on the App Store. This applies to both paid apps and in-app purchases in free apps.
1. Go to the **Business** page in [App Store Connect](https://appstoreconnect.apple.com/business).
2. Find the **Paid Apps** agreement and click **Review and Agree**.
3. Complete the required information:
- **Banking information**: Add a bank account where Apple will send your earnings.
- **Tax information**: Fill in the tax forms for the countries where you want to sell.
- **Contact information**: Provide your contact details.
:::important
You must complete all three sections (banking, tax, contact) for the agreement to become active. Until the agreement is active, you cannot sell in-app purchases.
:::
### Create a Bundle ID
A Bundle ID uniquely identifies your app across the Apple ecosystem. You need it to register your app in App Store Connect and to configure the Adapty integration.
1. Open the [Apple Developer portal](https://developer.apple.com/account).
2. Go to **Certificates, Identifiers & Profiles** → **Identifiers**.
3. Click **+** to register a new identifier.
4. Select **App IDs** and click **Continue**.
5. Select **App** as the type and click **Continue**.
6. Fill in the fields:
- **Description**: A name to help you identify this Bundle ID (e.g., "My Subscription App").
- **Bundle ID**: Choose **Explicit** and enter a unique identifier in reverse-domain format (e.g., `com.yourcompany.yourapp`).
7. In the **Capabilities** section, scroll down and check **In-App Purchase**.
8. Click **Continue**, then **Register**.
### Register your app in App Store Connect
1. Go to the **Apps** page in [App Store Connect](https://appstoreconnect.apple.com/apps).
2. Click **+** → **New App**.
3. Fill in the required fields:
- **Platforms**: Select **iOS**.
- **Name**: Your app's name as it will appear on the App Store.
- **Primary language**: The default language for your app's metadata.
- **Bundle ID**: Select the Bundle ID you created in the previous step.
- **SKU**: A unique identifier for your app (not visible to users). For example, `my_subscription_app_2025`.
4. Click **Create**.
Your app is now registered in App Store Connect and ready for the Adapty integration.
## What's next
- [Initial integration with the App Store](initial_ios): Connect your App Store app to Adapty
- [SDK integration](quickstart-sdk): Integrate the Adapty SDK into your app code
- [Sandbox testing](test-purchases-in-sandbox): Test your in-app purchases before release
- [Submit your iOS app to the App Store](submit-app-to-app-store): Upload your build and submit for Apple review
- [App Store Small Business Program](app-store-small-business-program): Reduce your App Store commission from 30% to 15%
---
# File: app-store-products
---
---
title: "Product in App Store"
description: "Manage App Store products efficiently using Adapty’s subscription tools."
---
This page provides guidance on creating a product in App Store Connect. While this information may not directly pertain to Adapty's functionality, it serves as a valuable resource if you encounter challenges while creating products in your App Store Connect account.
To create a product that will be linked to Adapty:
1. Open **App Store Connect**. Proceed to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) section in the left-side menu.
2. If you haven't created a subscription group, click the **Create** button under the **Subscription Groups** title to initiate the process. [Subscription Groups](https://developer.apple.com/help/app-store-connect/manage-subscriptions/offer-auto-renewable-subscriptions) in App Store Connect categorize and manage your products, allowing users to switch between different offerings seamlessly. Note that it's not possible to create a subscription outside of a group.
3. In the opened **Create Subscription Group** window, enter a new subscription group name in the **Reference Name** field. The reference name is a user-defined label or identifier that helps you distinguish and manage different subscription groups within your app.
The reference name is not visible to users; it's primarily for your internal use and organization. It allows you to easily identify and refer to specific subscription groups when managing them within the App Store Connect interface. This can be particularly useful if you have multiple subscription offerings or want to categorize them in a way that makes sense for your app's structure.
4. Click the **Create** button to confirm the subscription group creation.
5. The subscription group is created and opened. Now you can create subscriptions in the group. Click the **Create** button under the **Subscriptions** title. If you add a new subscription to an existing group, then click a **Plus** button next to the **Subscriptions** title.
6. In the opened **Create Subscription** window, enter its name in the **Reference Name** field and subscription unique code in the **Product ID** field.
The Reference Name serves as an exclusive identifier within App Store Connect for your in-app subscription. It is not visible to your users on the App Store. We recommend using a clear, human-readable description that accurately represents the specific subscription you intend to create. Please note that this name must not exceed 64 characters in length.
The Product ID is a unique alphanumeric identifier essential for accessing your product during the development phase and synchronizing it with Adapty, a service designed to manage in-app subscriptions. Only alphanumeric characters, periods, and underscores are allowed in the Product ID.
7. Click the **Create** button to confirm the subscription creation.
8. The subscription is created and opened. Now select the duration of the subscription in the **Subscription Duration** list. Even if the subscription duration is already indicated in the subscription name, remember to complete the **Subscription Duration** field.
9. Now it's time to set up the subscription price. To do so, click the **Add Subscription Price** button under the Subscription Prices title. You may need to scroll down to find them.
10. In the opened **Subscription Price** window, select the basic country in the **Country or Region** list and and basic currency in the **Price** list. Later Apple will automatically calculate the prices for all 175 countries or regions based on this basic price and the most recent foreign exchange rates.
11. Click the **Next** button. In the opened **Price by Country or Region** window, you see the automatically recalculated prices for all countries. You can change them if you want.
12. After updating regional prices, proceed by clicking the **Next** button at the bottom of the window.
13. In the opened **Confirm Subscription Price?** window, carefully review the final prices. To correct the prices, you can click the **Back** button to return to the **Price by Country or Region** window and update them. When you are ok with the prices, click the **Confirm** button.
14. After closing the **Confirm Subscription Price?** window, remember to click the **Save** button in your subscription window. Without it, the subscription won't be created, and all entered data will be lost.
Please consider, that the steps provided so far focus on configuring an Auto-Renewable Subscription. However, if you intend to set up other types of in-app purchases, you can click on the **In-App Purchases** tab in the sidebar, instead of "Subscriptions." This will lead you to the section where you can manage and create various types of in-app purchases.
### Add products to Adapty
Once you have completed adding your in-app purchases, subscriptions, and offers in App Store Connect, the next step is to [add these products to Adapty](create-product).
---
# File: apple-app-privacy
---
---
title: "Apple App Privacy"
description: "Understand Apple app privacy policies and their impact on your subscription app."
---
Apple requires a privacy disclosure for all new apps and app updates both in the **App Privacy** section of App Store Connect and as the app manifest file. Adapty is a third-party dependency to your app, so you need to disclose how you use Adapty in relation to user data.
## Apple app privacy manifest
The [privacy manifest file](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests), named `PrivacyInfo.xcprivacy`, describes what private data your app uses and why. You as every app owner must create a manifest file for your app. Additionally, if you're integrating any extra SDKs, ensure the manifest files for those of them included in the [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/) list are included. When you build your app, Xcode will take all these manifest files and merge them into one.
Even though Adapty isn't on the list of [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/), versions 2.10.2 and higher of the Adapty SDK include it for your convenience. Make sure to update the SDK to get the manifest.
While Adapty doesn't require any data to be included in the manifest file also called app privacy report, if you're using Adapty's ` customerUserId` for tracking, it's necessary to specify it in your manifest file like so:
1. Add a dictionary to the `NSPrivacyCollectedDataTypes` array in your privacy information file.
2. Add the `NSPrivacyCollectedDataType`, `NSPrivacyCollectedDataTypeLinked`, and `NSPrivacyCollectedDataTypeTracking` keys to the dictionary.
3. Add string `NSPrivacyCollectedDataTypeUserID` (identifier of the `UserID` data type in the [List of data categories and types to be reported in the manifest file](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests#4250555)) for the `NSPrivacyCollectedDataType` key in your `NSPrivacyCollectedDataTypes` dictionary.
4. Add `true` for the `NSPrivacyCollectedDataTypeTracking` and `NSPrivacyCollectedDataTypeLinked` keys in your `NSPrivacyCollectedDataTypes` dictionary.
5. Use the `NSPrivacyCollectedDataTypePurposeProductPersonalization` string as the value for the `NSPrivacyCollectedDataTypePurposes` key in your `NSPrivacyCollectedDataTypes` dictionary.
If you target your paywalls to audiences with custom attributes, consider carefully what custom attributes you use and if they match the [data categories and types to be reported in the manifest file](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests#4250555). If so, repeat the steps above for every data type.
After you report all data types and categories you collect, create your app's privacy report as described in [Apple documentation](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests#4239187).
## Apple app privacy disclosure in App Store Connect
1. In [App Store Connect](https://appstoreconnect.apple.com/), open your app and go to **App Privacy**. Click **Get Started**.
2. Select **Yes, we collect data from this app** and click **Next**.
### Data types
The table below lists data types that Apple requires you to disclose and indicates which ones Adapty needs. **This only covers Adapty.** If your app collects additional data through other SDKs or your own code, select those data types as well.
✅ = Required by Adapty
👀 = May be required \(see details below\)
❌ = Not required by Adapty — select if your app collects this data through other means
| Data type | Required | Note |
|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| Identifiers | ✅ | If you are identifying users with a customerUserId, select 'User ID'.
Adapty collects IDFA, so you have to select 'Device ID'.
| | Purchases | ✅ | Adapty collects purchase history from users. | | Contact Info, including name, phone number, or email address | 👀 | Required if you pass personal data like name, phone number, or email address using **`updateProfile`** method. | | Usage Data | 👀 | If you are using analytics SDKs such as Amplitude, Mixpanel, AppMetrica, or Firebase, this may be required. | | Location | ❌ | Adapty does not collect precise location data. Select if your app collects it. | | Health & Fitness | ❌ | Adapty does not collect health or fitness data. Select if your app collects it. | | Sensitive Info | ❌ | Adapty does not collect sensitive information. Select if your app collects it. | | User Content | ❌ | Adapty does not collect user content. Select if your app collects it. | | Diagnostics | ❌ | Adapty does not collect diagnostic data. Select if your app collects it. | | Browsing History | ❌ | Adapty does not collect browsing history. Select if your app collects it. | | Search History | ❌ | Adapty does not collect search history. Select if your app collects it. | | Contacts | ❌ | Adapty does not collect contact lists. Select if your app collects it. | | Financial Info | ❌ | Adapty does not collect financial info. Select if your app collects it. | ### Required data types #### Purchases When using Adapty, you must disclose that your app collects **Purchase History**.
#### Identifiers
When using Adapty, you must disclose the following identifiers:
- **Device ID** — Adapty collects IDFA.
- **User ID** — required if you identify users with **`customerUserId`**.
### Data usage
After saving **Data types**, you'll need to indicate how the data is used:
1. Click **Set up purchase history** inside the **Purchases** block.
2. When Apple asks how purchase history data is used, select the following for Adapty:
- **Analytics** — Adapty uses purchase history for revenue analytics, cohorts, and metrics.
- **Product Personalization** — Adapty uses purchase data for audience segmentation and paywall targeting.
- **App Functionality** — Adapty validates purchases, manages access levels, and tracks subscription status.
Select additional purposes if your app uses purchase data in other ways (for example, if you send purchase events to ad platforms via Adapty integrations).
3. Click **Next**.
4. For both **Device ID** and **User ID** (if used):
1. Click **Set up user/device ID** inside the **User/Device ID** block.
2. When Apple asks how identifier data is used, select the following for Adapty:
- **App Functionality** — Adapty uses identifiers to manage user profiles, link purchases, and track access levels.
If you send attribution data to third-party platforms via Adapty integrations (such as AppsFlyer or Adjust), also select **Third-Party Advertising**. Select additional purposes if your app uses identifiers in other ways.
5. Click **Next**.
---
# File: apple-family-sharing
---
---
title: "Apple family sharing"
description: "Enable Apple Family Sharing in Adapty to support shared subscriptions."
---
Apple's family sharing enables the distribution of in-app purchases among family members, offering users of group-oriented apps, such as video streaming services and kids' apps, a convenient way to split subscriptions without having to share their Apple ID. By allowing up to five family members to utilize a subscription, [Family sharing](https://developer.apple.com/documentation/storekit/supporting-family-sharing-in-your-app) can potentially improve customer engagement and retention for your app.
In this guide, we will provide instructions on how to opt-in subscriptions to Family Sharing and explain how Adapty manages purchases that are shared within a family.
To get started with enabling Family Sharing for a particular product, head over to [App Store Connect](https://appstoreconnect.apple.com/). Family Sharing is turned off by default for both new and existing in-app purchases, so it is necessary to enable it individually for each in-app purchase. You can easily do this by accessing your **app's page,** navigating to the corresponding in-app purchase page, and selecting the **Turn On** option in the Family Sharing section.
Keep in mind that once you enable Family Sharing for a product, **it cannot be turned off again**, as this would disrupt the user experience for those who have already shared the subscription with their family members.
Also, please consider that, only non-consumables and subscriptions can be shared.
On the displayed modal simply click on the **Confirm** button to finalize the setup process. After doing so, the Family Sharing section should update to display the message, "This subscription can be shared by everyone in a family group." This confirms that the subscription is now enabled for Family Sharing and can be shared among up to five family members.
Adapty makes it easy to support Family Sharing without any additional effort required. You just need to simply [configure your products](app-store-products) from the App Store, and once you **enable** it from App Store Connect **Family Sharing** will be automatically available in **Adapty**, that will be received as an event on the webhook.
:::note
Please note that Family Sharing is not supported in the sandbox environment.
:::
One thing you can consider is that when a user purchases a subscription and shares it with their family members, there is a **delay of up to one hour** before it becomes available to them. Apple designed this delay to give the user time to change their mind and undo the sharing if they want to. However, if the subscription is renewed, there is no delay in making it available to the family members.
When a user purchases a Family Shareable in-app product, the transaction will appear in their receipt as usual, but with the addition of a new field called `in_app_ownership_type` with the value `PURCHASED.` Furthermore, a new transaction will be created for all family members, which will have a different `web_order_line_item_id` and `original_transaction_id` compared to the original purchase, as well as an `in_app_ownership_type` field with the value `FAMILY_SHARED.`
To ensure accurate revenue calculation, on the Adapty side, only transactions with an `in_app_ownership_type` value of `PURCHASED` are considered. This means that we don't take into account `FAMILY_SHARED` values in analytics and do not send events based on them.
To identify the other family members on Adapty, you can find them in the event details. First, locate the original family purchase transaction. Then, examine the event details for that transaction, specifically looking for the same product, purchase date, and expiration date. By analyzing the event details, you can identify other family membership transactions associated with the original purchase.
---
# File: app-store-small-business-program
---
---
title: "App Store Small Business Program"
description: "Understand Apple's Small Business Program, its impact on your revenue and Adapty's analytics"
---
:::link
For the corresponding Play Store program, see [Google Reduced Service Fee](google-reduced-service-fee).
:::
Organizations that receive up to 1 million USD in yearly App Store proceeds are eligible to participate in Apple's [Small Business program](https://developer.apple.com/app-store/small-business-program/). If you enroll, the standard 30% store commission rate is lowered to **15%**.
Program members must **change their Adapty settings** to ensure correct revenue calculations and integration event handling.
This article describes:
* [How to set up Adapty](#configure-adapty) if your app is enrolled in the Small Business Program
* [How to enroll in the program](#apply-for-the-program) if you want to reduce your store commission
## Configure Adapty
Adapty can apply the reduced commission rate to your [analytics](analytics) and [integration events](analytics-integration). To enable this, specify your Small Business Program status on a per-app basis.
:::warning
To ensure a seamless transition, configure your SBP status in Adapty **as soon as you receive approval**.
If you retroactively reduce the commission rate, Adapty will change the analytics data, but won't repeat past transactions' webhooks and integration events.
:::
1. Open [**App Settings** → **General**](https://app.adapty.io/account)
2. Find the **Small Business Program** section.
3. Click **Add period**.
4. Select the membership start date.
5. Select an end date, or enable the **At the current moment** checkmark to indefinitely extend this status. If you [lose eligibility](#losing-eligibility) in the future, you can modify the end date.
6. Click **Apply**.
If your organization remains eligible for the program, its membership carries over to the next calendar year. But the membership status only applies **to the date range you specify**.
* Click **Add period** to add a new membership period.
* To extend this status indefinitely, enable the **At the current moment** checkmark.
To verify your configuration, open the [Revenue chart](revenue) and select **Proceeds after store commission**. Confirm that the displayed proceeds reflect the reduced commission rate.
## Apply for the program
### Eligibility requirements
Apple determines SBP eligibility based on your **yearly proceeds** — the previous calendar year's sales **after** store commission and taxes.
To be eligible, the yearly proceeds of your organization and its
2. Click the **Create subscription** button.
3. In the opened **Create subscription** window, enter the subscription ID in the **Product ID** field and the subscription name in the **Name** field.
Product ID has to be unique and must start with a number or lowercase letter, and can also contain underscores (\_), and periods (.). It is used to access your product during development and synchronize it with Adapty. Once a Product ID is assigned to a product in the Google Play Console, it cannot be reused for any other apps, even if the product is deleted.
When naming your product ID, it is advisable to follow a standardized format. We recommend using a more concise approach and naming the product`
3. After the subscription details open. click on the **Add base plan** button under the **Base plans and offers** title. You may need to scroll down to find it.
4. In the opened **Add base plan** window, enter a unique identifier for the base plan in the Base **Plan ID** field. It must start with a number or lowercase letter, and can contain numbers (0-9), lowercase letters (a-z) and hyphens (-). and complete the required fields.
5. Specify the prices per region.
6. Сlick the **Save** button to finalize the setup.
7. Сlick the **Activate** button to make the baseline active.
Keep in mind that subscription products can only have a single base plan with consistent duration and renewal type in Adapty.
### Fallback products
:::warning
Support for non backwards-compatible base plans
Older versions of Adapty SDKs do not support Google Billing Library v5+ features, specifically multiple base plans per subscription product and offers. Only base plans marked as **[backwards compatible](https://support.google.com/googleplay/android-developer/answer/12124625?hl=en#backwards_compatible)** in the Google Play Console are accessible with these SDK versions. Note that only one base plan per subscription can be marked as backwards compatible.
:::
To fully leverage the enhanced Google subscription configurations and features in Adapty, we offer the capability to set up a backward compatible fallback product. This fallback product is exclusively utilized for apps using older versions of the Adapty SDK. When creating Google Play products, you now have the option to indicate whether the product should be marked as backward compatible in the Play Console. Adapty utilizes this information to determine whether the product can be purchased by older versions of the SDK (versions 2.5 and below).
Suppose you have a subscription named `subscription.premium` that offers two base plans: weekly (backward compatible) and monthly. If you add `subscription.premium:weekly` product to Adapty, you don't need to indicate a backward compatible product. However, in the case of `subscription.premium:monthly` product, you will need to specify a backward compatible product. Failing to do so could result in an unintended purchase of `subscription.premium:weekly` product in Google 4th billing library. To address this scenario, you should create a separate product where the base plan is also monthly and marked as backward compatible. This ensures that users who select the `subscription.premium:monthly` option will be billed correctly at the intended frequency.
## Add products to Adapty
Once you have completed adding your in-app purchases, subscriptions, and offers in App Store Connect, the next step is to [add these products to Adapty](create-product).
---
# File: google-play-data-safety
---
---
title: "Google Play Data Safety"
description: "Ensure compliance with Google Play Data Safety policies in Adapty."
---
The Data Safety section available on Google Play provides a simple method for app developers to inform users about the data collected or shared by their app, as well as highlight their app's critical privacy and security measures. This information enables users to make more informed decisions when selecting which apps to download and use.
Here is a short guide on data that Adapty collects to help you provide the required information to Google Play.
## Data Collection and Security
**Does your app collect or share any of the required user data types?**
Select 'Yes' as Adapty collects a customer's purchase history.
**Is all of the user data collected by your app encrypted in transit?**
Select 'Yes' as Adapty encrypts data in transit.
**Do you provide a way for users to request that their data is deleted?**
If selecting 'Yes', ensure your customers have a way to contact your support team to request a data deletion. You will be able to delete the customer directly from the Adapty dashboard or via REST API.
## Data Types
Here is a list of the data types that Google requires for reporting, and we have specified whether Adapty collects any particular type of data.
| Data Type | Details |
| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Location | Is not collected by Adapty |
| Health and Fitness | Is not collected by Adapty |
| Photos and Videos | Is not collected by Adapty |
| Files and Docs | Is not collected by Adapty |
| Calendar | Is not collected by Adapty |
| Contacts | Is not collected by Adapty |
| User Content | Is not collected by Adapty |
| Browsing History | Is not collected by Adapty |
| Search History | Is not collected by Adapty |
| App Info and Performance | Is not collected by Adapty |
| Web Browsing | Is not collected by Adapty |
| Contact Info | Is not collected by Adapty |
| Financial Info | Adapty collects purchase history from users |
| Personal Info and Identifiers | Adapty collects User ID and some other identifiable contact information including name, email address, phone number, etc, if you explicitly pass them to Adapty SDK. |
| Device and other identifiers | Adapty collects data on device id. |
## Data usage and handling
### User IDs
**1. Is this data collected, shared, or both?**
This data is collected by Adapty. If you are using integrations set up between Adapty and third parties that are not considered service providers, you may need to disclose "Shared" here as well.
**2. Is this data processed ephemerally?**
Select 'No'.
**3. Is this data required for your app, or can users choose whether it's collected?**
This data collection is required and cannot be turned off.
**4. Why is this user data collected? / Why is this user data shared?**
Select the 'App functionality' and 'Analytics' checkboxes.
### Financial Info
If you are using Adapty, you must disclose that your app collects 'Purchase history' information from the Data types section in Google Play Console.
### Device or other IDs
## Next Steps
Once you have made your data safety selections, Google will display a preview of your app's privacy section. If you have opted for "Financial Info" and "Device or other IDs" as mentioned earlier, your privacy information should appear similar to the following example
If you are prepared to submit your app for App Review, please refer to our [Release Checklist](release-checklist) document for further guidance on preparing your app for submission.
---
# File: google-reduced-service-fee
---
---
title: "Google Reduced Service Fee"
description: "Understand Google's Reduced Service Fee, its impact on your revenue and Adapty's analytics"
---
:::link
For the corresponding App Store program, see [App Store Small Business Program](app-store-small-business-program).
:::
Google Play's [Reduced Service Fee program](https://support.google.com/googleplay/android-developer/answer/112622?hl=en) lowers the commission on your first 1 million USD in yearly earnings from 30% to **15%**. Earnings above 1 million USD in the same calendar year are charged at the standard 30% rate.
:::note
Since January 1, 2022, Google charges 15% on all auto-renewable subscriptions regardless of this program. The Reduced Service Fee primarily benefits non-subscription in-app purchases and paid apps.
:::
Program members must **change their Adapty settings** to ensure correct revenue calculations and integration event handling.
This article describes:
* [How to set up Adapty](#configure-adapty) if your app is enrolled in the Reduced Service Fee program
* [How to enroll in the program](#enroll-in-the-program) if you want to reduce your store commission
## Configure Adapty
Adapty can apply the reduced commission rate to your [analytics](analytics) and [integration events](analytics-integration). To enable this, specify your Reduced Service Fee status on a per-app basis.
:::warning
Configure your Reduced Service Fee status in Adapty **as soon as you enroll**.
If you retroactively reduce the commission rate, Adapty will change the analytics data, but won't repeat past transactions' webhooks and integration events.
:::
1. Open [**App Settings** → **General**](https://app.adapty.io/account).
2. Find the **Reduced Service Fee** section.
3. Click **Add period**.
4. Select the membership start date.
5. Select an end date, or enable the **At the current moment** checkmark to indefinitely extend this status. If your [annual earnings exceed 1 million USD](#exceeding-the-threshold), you can modify the end date.
6. Click **Apply**.
The membership status only applies **to the date range you specify**. The program resets each calendar year.
* Click **Add period** to add a new membership period.
* To extend this status indefinitely, enable the **At the current moment** checkmark.
To verify your configuration, open the [Revenue chart](revenue) and select **Proceeds after store commission**. Confirm that the displayed proceeds reflect the reduced commission rate.
## Enroll in the program
### Eligibility requirements
Google determines eligibility based on your **yearly earnings** across all accounts in your
## What is the Refund Saver?
When users request refunds on the App Store, Apple evaluates consumption data related to the in-app purchase to decide whether to approve or deny the request. For example, if a user buys a subscription, uses it heavily for most of the subscription period, and then requests a refund, Apple is likely to approve it unless you provide usage data to show the subscription was actively consumed. Apple [encourages developers](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1) to share this data to ensure refund decisions are fair.
Adapty’s Refund Saver automates this process while remaining fully compliant with App Store [guidelines](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1).
Here’s how it works:
- When a user initiates a refund request, the App Store sends a notification asking for transaction and usage details.
- If you ignore or delay the response, Apple is likely to approve the refund by default.
- Adapty automatically processes these notifications, providing Apple with the necessary data.
This automation reduces the chance of unnecessary refunds while saving you time and protecting your revenue.
:::info
With Refund Saver, you can save up to 40% of the revenue from refund requests.
:::
## Requirements to use Refund Saver
To use this feature, ensure you’ve met the following prerequisites:
1. **Update your Privacy Policy in App Store Connect:**
Your app’s Privacy Policy must disclose the collection and use of consumption data. This ensures users understand your app’s privacy practices before downloading it. Refer to [Apple’s App Privacy Details](https://developer.apple.com/app-store/app-privacy-details/) for guidance
2. **Obtain user consent for data sharing in your app**:
Apple insists that you must obtain valid consent from the user before sharing their personal data with Apple. As the developer, you’re responsible for obtaining this consent since you’ll be sharing user data with Apple. See Apple’s [guidelines](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1#3921151) for more details.
3. **Enable Server Notifications V2:**
Ensure that Server Notifications V2 are activated in your Apple Developer account and properly configured in Adapty, as V1 notifications are not supported. If they aren’t activated yet, follow the steps in the [Enable App Store server notifications](enable-app-store-server-notifications) guide
## Turn on Refund Saver
1. Open the [Refund Saver](https://app.adapty.io/refund-saver) section in the Adapty Dashboard.
2. Click **Turn on Refund Saver** to activate the feature.
## Set a default refund behavior
Apple allows developers to specify a preferential outcome for each refund request when responding to it. The purpose of this setting is to find the right balance between declining and accepting refund requests so that only fair refunds are provided. Note that this setting is only used to influence an outcome, but ultimately the decision is still up to Apple.
Adapty supports setting this preference, but we will use the same value for every refund request.
1. To change your preference, click **Edit refund preference**.
2. In the **Edit refund preference** window, choose your **Default refund request preference** option:
| Option | Description |
| -------------------------------------------- | ------------------------------------------------------------ |
| Always decline | (default) This is the default option and usually yields the best results for minimizing refunds. |
| Decline first refund request, grant all next | For every transaction Refund Saver encounters, it will initially ask Apple to decline the refund. However, if the same transaction appears again, Refund Saver will always recommend granting the refund. This approach helps minimize user frustration from unfair refund declines — users can simply request the refund again and will likely receive it. |
| Always refund | Suggests that Apple approve every refund request. |
| No preference | Do not provide any recommendations to Apple. In this case, Apple will determine the refund outcome based on its internal policies and user history, without any influence from your settings. This option provides the most neutral approach. |
## Set refund behavior for a specific user in the dashboard
Even if you’ve configured the default Refund Saver behavior for the entire app, you may want to set individual preferences for specific users. In the Adapty Dashboard, you can do this from the user’s profile. Use the **Refund Saver Preferences** section located at the bottom left.
## Set refund behavior for a specific user in the SDK
You can set the refund preference in your app code individually for every installation depending on some user's actions. Use the snippet below to set the preference:
| Option | Description | | ------- | ------------------------------------------------------------ | | Opt-out | (default) If Adapty doesn't know the user's consent status, it assumes consent **was given** and Refund Saver **will share** refund-related data with Apple. | | Opt-in | If Adapty doesn't know the user's consent status, it assumes consent **was not given** and Refund Saver **won’t share** any data with Apple. This is Apple’s recommended approach. | ## Update user consent in the SDK To tell Adapty whether a specific user has given consent, use the `updateCollectingRefundDataConsent` method:
:::note
You can also use the Server-side API to [get individual refund and sharing preferences](api-adapty/operations/getRefundSaverSettings.md).
:::
## Limitations
- **Apple's App Store only:** Refund Saver is only available for refund requests made to Apple's App Store. Google Play doesn't offer consumption data analysis for refunds. Refund decisions on Google Play are based solely on Google's policies and the information provided by the user.
- **Auto-renewable subscriptions and consumables only:** Refund Saver works with auto-renewable subscriptions and consumable in-app purchases, as Apple only provides required information for these purchase types.
- **Requires Server Notifications V2:** Refund Saver is not compatible with App Store Server Notifications V1. If you're currently using V1 in Adapty, you need to switch to V2, see the [Sending App Store server notifications to Adapty](enable-app-store-server-notifications) guide for details. Switching to V2 will also improve your analytics in Adapty by providing more accurate and comprehensive data.
---
# File: meta-create-campaign
---
---
title: "Advertise your app in Meta Ads"
---
In this step-by-step guide, you will learn how to create and set up ads for your app in Meta, so you can optimize them and track their performance easily.
## How ads in Meta are structured
When advertising on Meta Ads, you need to configure three hierarchical levels:
- **Campaign**: Campaigns define your advertising objectives.
- **Ad set**: Ad sets specify your target audience and placements—determining where and to whom your ads will be displayed. Each campaign can contain multiple ad sets.
- **Ads**: Ads are the actual creatives that users see and interact with. Each ad set can contain several ads; however, it is recommended to limit this to no more than five ads per ad set for optimal performance.
## Step 1. Create Meta Ads Manager Account
To get started with Meta Ads, you need to have a Facebook Business page because you can't run ads from your personal one.
So, you need to link your business page to your Meta Ads business portfolio:
1. Go to [business.facebook.com](https://business.facebook.com/). If you don't have a business page in the business portfolio yet, you need to add it. Click **Go to settings**.
2. Go to **Account > Pages** from the left sidebar. Click **Add** and select **Add an existing Facebook page** or **Create a new Facebook page**. See the [guide on creating a business page](https://www.facebook.com/business/help/473994396650734) if you don't have one yet.
3. Optionally, attach your Instagram account on the **Account > Instagram accounts** page in the settings.
Once you've connected your business page, you're ready to move further.
## Step 2. Add Meta pixel
You will need a Meta pixel to connect your campaign data to revenue and get better results.
Before you connect data and create a pixel, you will need:
- A business page – add it to your business portfolio in [**Settings > Accounts > Pages**](https://business.facebook.com/latest/settings/pages)
- A business manager account – you must have full control over the business portfolio
- A business email – set in [**Settings > Business info**](https://business.facebook.com/latest/settings/business_info)
- An ad account – add it to your business portfolio in [**Settings > Accounts > Ad accounts**](https://business.facebook.com/latest/settings/ad_accounts)
When you're ready, create a pixel:
1. Go to [**Events Manager**](https://www.facebook.com/events_manager2). Click **Connect data**.
2. Select **Web** as a data source type.
3. Give your dataset a name and click **Create**.
4. For [Adapty User Acquisition](adapty-user-acquisition.md)), you won't need to complete the full installation of the pixel. So, when asked about the integration, you can just click **Cancel** in the setup window, and your pixel will still appear on the list.
5. When your dataset appears on the list, you can proceed with creating a campaign.
## Step 3. Create campaign
To create a campaign in Meta Ads Manager:
1. Go to [Meta Ads Manager](https://adsmanager.facebook.com/adsmanager/manage). On the **Campaign** tab, click **Create**.
2. Select **Sales** as a campaign objective and click **Continue**.
3. Name your campaign in the **Campaign name** section.
4. In the **Budget** section, in **Budget strategy**, select how you want to control the budget:
- **Campaign budget**: The easiest option if you are not sure which opportunities would work best. If you select this, Meta Ads will automatically detect top performers to allocate more budget on better-performing ad sets.
Then, select whether you need **Daily** or **Lifetime** budget and enter the limit in your currency. **Daily** budget allows you more flexibility while you are still learning, so you can start with smaller amounts and gradually adjust them on the go. Or, you can select **Schedule budget increase** and set rules to automatically increase the budget by value amount or percentage.
- **Ad set budget**: Select this option if you want to manually define which audiences will get more or less campaign budget. If not completely sure, you can select **Share some of your budget with other ad sets** to allow Meta to automatically adjust ad set budgets by up to 20% if it benefits the ad performance.
5. In **Campaign bid strategy**, select the best option for your goals:
- **Highest volume (default)**: The easiest option to get started. If you select this, you let Meta optimize the click cost to get the best results for your budget.
- **Cost per result goal**: Aim for a certain cost per result if you know your benchmarks.
- **Bid cap**: Set the highest cost you are ready to bid.
6. Adapty lets you conduct comprehensive [A/B tests](ab-tests.md). However, you can also enable A/B tests in Meta Ads if needed. Read more about A/B tests in Meta Ads Manager [here](https://www.facebook.com/business/help/1159714227408868).
7. Now, it's time to add the first ad set to your campaign. Click **Next** to proceed.
## Step 4. Create ad set
To create an ad set:
1. Give your ad set a name in the **Ad set name** field.
2. In the **Conversion location** dropdown, select **Website**.
3. In the **Performance goal** field, select **Maximize number of landing page views** if you have a landing page or **Maximize number of link clicks** if you use a smart link navigating users directly to the store.
4. In the **Dataset** field, select the dataset you've created on [Step 2](#step-2-add-meta-pixel).
5. Select a **Conversion event**. In our case, it is probably going to be **Purchase** or **Start trial**. Don't worry if you see a warning that your dataset doesn't have any events yet – that just means that your dataset is new.
6. If, when setting up the campaign, you have selected **Ad set budget**, select whether you need **Daily** or **Lifetime** budget and enter the limit in your currency. **Daily** budget allows you more flexibility while you are still learning, so you can start with smaller amounts and gradually adjust them on the go.
Set the start and, if applicable, end dates for the ad set. For example, if you want to advertise a promotional offer in your app, it is crucial to align the ad set timeframe with the offer.
7. In the **Audience controls** section, set the audience settings:
- **Location**: Locations can be as broad or narrow as you need them to be. You can limit **Locations** in the ad set to work with the region specifics in your ads.
- **Minimum age**: Select the minimum age of users that will see your ad. For some ads, it may be legally required. You can't select a minimum age below 18 globally or 20 in Thailand.
- **Language**: Set **Language** only if it is not the most common language in selected countries. For example, you won't need to select **English** in the United States, but, if you target Spanish-speaking people living there, you might want to select **Spanish**.
8. By default, Meta automatically finds smaller groups of people to whom your ad will be relevant. However, if you add an audience suggestion, you can guide Meta towards people you think are likely to respond. In the **Advantage+ audience** section, you can adjust:
- **Age**: Set a specific age range to target, so you better match specific of different age groups.
- **Gender**: Show your ad to all users or target them by their gender.
- **Detailed targeting**: This setting allows you the most specific control over the audience for your ad and/or app. Here, you can form groups based on **Demographics**, **Interests**, or **Behaviors**. Depending on what your app is doing, for example, you can focus on different professions, fans of specific music bands, parents of newborns, or those who tend to shop online a lot.
:::note
The **Detailed targeting** settings apply with the **Or** operator. If you want to apply conditions with the **And** operator, click **Define further** and select new conditions.
:::
9. In the **Placements** section, you can select where your ad will appear. By default, the **Advantage+** setting is selected, letting Meta allocate your ad set's budget across multiple placements based on where they're likely to perform best. We recommend you to use this option if you are not sure where to place your ad. If you want to select specific placements manually, select **Manual placements** and customize them. Read more [here](https://www.facebook.com/business/help/965529646866485).
10. **Recommended**: Targeting by device helps you optimize your spending. In the **Placements** section, click **Show more settings**. In the **Devices and operating system** subsection, select which devices, operating systems, and OS versions should be included in your audience. This ensures your ads are shown only to relevant users. For example, desktop users won't see your ad, and users with old OS versions that your app doesn't support will be excluded.
11. When you're ready, click **Next** to proceed.
## Step 5. Create ads
To create an ad in Meta Ads Manager:
1. Give your ad a name in the **Ad name** field.
2. In the **Identity** section, select the Facebook page that will be used for posting ads. If you have a separate Instagram account for your app and have connected it in Meta Business Suite at [Step 1](#step-1-create-meta-ads-manager-account), select it in the **Instagram account** dropdown. Otherwise – select **Use Facebook page**, so Instagram ads are posted using the Facebook page.
3. In **Ad setup**, select how you want to post your ad. When advertising apps, we recommend selecting **Create ad**, so your post will redirect users to your app instead of the Facebook page. In the **Format** field, select an option depending on how many creatives you have and how you want to display them.
4. In the **Destination** section, keep **Website** selected as **Main destination**. In the **Website URL** field, paste `https://api-ua.adapty.io/api/v1/attribution/click`. In [Adapty User Acquisition](adapty-user-acquisition.md), [create a web campaign](ua-facebook.md) and paste the **Click link** content after `https://api-ua.adapty.io/api/v1/attribution/click` to the **URL parameters** field in the **Tracking** section.
5. In the **Ad creative** section, click **Set up creative** and select **Image ad** or **Video ad**. This will open a new window prompting you to upload media files, crop them, and add texts.
6. If you want to automatically translate your ad texts, in the **Languages** section, click **Add languages**. Then, add a primary language – it will automatically pull texts from your creative. Then, add translation languages for automatic translation.
7. When ready, click **Publish** to launch your ad.
## What's next
To activate your ad, you will need to add a payment method if you haven't done it before.
Then, you can [explore how the campaign affects your app revenue in the Adapty User Acquisition dashboard](adapty-user-acquisition.md).
Not using Adapty User Acquisition yet? [Book a call with us](https://calendly.com/tnurutdinov-adapty/30min) to learn how it can help you track and optimize your ad campaigns.
---
# File: tiktok-create-campaign
---
---
title: "Advertise your app in TikTok for Business"
---
In this step-by-step guide, you will learn how to create and set up ads for your app in TikTok for Business, so you can optimize them and track their performance easily.
## Step 1. Add business info
If you are just getting started with TikTok for Business, you need to add your business info first:
1. Go to [https://ads.tiktok.com](https://ads.tiktok.com/business/) and click **Get started**.
2. Sign up using your email or your TikTok account.
3. Enter your business info and follow the prompts on the screen.
Once your business account is approved, you will be redirected to creating your first campaign.
## Step 2. Create a pixel
You will need a TikTok pixel to connect your campaign data to revenue and get better results:
1. Go to [**Events Manager**](https://ads.tiktok.com/i18n/events_manager/home). Click **Connect data source**.
2. Select **Web** as a data source type.
3. In the **Add your website** window, click **Skip**.
4. Select **Manual setup** and click **Next**.
5. Select **TikTok pixel + Events API** and click **Next**.
6. Give your pixel a name and click **Create**.
7. For [Adapty User Acquisition](adapty-user-acquisition.md), you won't need to complete the full installation of the pixel. So, you can just close the setup window, and your pixel will appear on the list.
8. To make this pixel available for use in campaigns, you need to send a test event to it from [Adapty User Acquisition](adapty-user-acquisition.md):
1. [Create a new TikTok campaign](ua-tiktok.md).
2. Expand a platform-specific section – e.g., iOS.
3. Select a pixel from the dropdown.
4. Click **Send test event**.
5. In the dropdown, select an event you will be using for optimization in the ad.
6. In TikTok for Business, open your pixel and switch to the **Test events** tab. Copy `test_event_code`.
7. Paste it to the **Test event code** field in Adapty and click **Send**.
9. The test event will appear in TikTok in several minutes. When you see it in your pixel details, you can proceed with the campaign setup in TikTok Ads Manager.
## Step 3. Select the campaign objective
:::important
This tutorial uses the Quick setup view in TikTok Ads Manager. A few recommended settings appear only in the Full view, which we point out in the relevant steps.
:::
Go to the [ad creation page](https://ads.tiktok.com/i18n/nb_creation/create/objectives) in the Ads Manager.
On the first screen, select the advertising objective and click **Continue**.
Select **Sales > Website conversion**.
## Step 4. Fill in the campaign info
Next, fill in the campaign info:
1. Name your campaign in the **Campaign name** field.
2. In the **Optimization goal** field, select **Conversion**.
3. Select your active pixel from the dropdown and choose an **Optimization event**. Note that only active events are available for selection. If the event you need is unavailable, send a test event following the instructions in [Step 2](#step-2-create-a-pixel).
4. Your ad will be displayed in the TikTok feed and search. For additional setup, click **Advanced settings**. In **Placements**, configure the placement settings:
- **User comment**: Select if you want to display your ad in the comments section as well. TikTok recommends keeping user comments on to help your ads achieve more impressions.
- **Allow video download**: Allow viewers to download your ad.
- **Allow video sharing**: Allow viewers to share your ad.
5. Click **Continue**.
## Step 5. Add ad content
Now, it's time to set up your creatives and destination URL:
1. In the **TikTok account** field, select an account that will be used for posting.
2. In [Adapty User Acquisition](adapty-user-acquisition.md), [create a web campaign](ua-tiktok.md) and paste **Click link** to the **Destination URL** field.
3. In the **Creatives** section, click **+ Videos and images**.
4. If you want to use your TikTok posts as creatives, select them on the **TikTok post tab**. Otherwise, switch to the **Creative library** tab and click **Upload**. Files you upload there will be accessible from this tab later, so you can reuse them in other campaigns.
5. Crop the creatives to fit the TikTok format and select whether they will be used as single ads or as one carousel.
6. Expand the upload creative and click **+** next to **No music selected**. You can upload your own mp3 files there. Adding music is required.
7. In the **Add text** field, enter the text that will be used as a description.
8. Select **Place the ads on this TikTok account as a post** if you want to post this ad in your TikTok account.
9. In the **Call to action** field, select or remove calls to action that are relevant to your ad. They will be appended to your ad automatically by TikTok.
10. Click **Continue**.
## Step 6. Configure targeting and budget
Finally, set up who should see your ad and how much you plan to pay for it:
1. In the **Targeting** section, select **Automatic** or **Custom**. The **Automatic** option is the easiest if you don't understand your audience yet. However, if you select **Custom**, you can optimize your spending by selecting those user groups who are more likely to respond to your ad.
2. If you have selected **Custom**, configure:
- **Location**: The default location is the location of your ad account. If you select more than one targeting country or region, ad review results will be returned separately for each location. Actual ads delivery may also vary depending on the supported locations of different placements.
- **Languages**: By default, all languages are selected. Select the targeting language based on the language used most often in your selected location.
- **Gender**: By default, all genders are selected.
:::tip
If you switch to Full mode, you’ll find an additional **Device** section under **Targeting**. Here you can limit your audience by device type, OS, and OS version—useful if your app requires a minimum version.
:::
3. In the **Budget** section, select one of the suggested options or select **Custom**.
4. If you have selected **Custom**, select whether you need **Daily** or **Lifetime** budget and enter the limit in your currency. **Daily** budget allows you more flexibility while you are still learning, so you can start with smaller amounts and gradually adjust them on the go.
5. In the **Schedule** section, select **Continue for at least 7 days** or **Custom**. We recommend you to set **Custom** schedule if your ad is time-sensitive, so you don't miss the moment when you need to stop it.
6. If you have selected **Custom**, set the start or start and end time for the ads. Note that your account timezone will be used.
7. Click **Publish**.
When you finish, it will create a new campaign with one ad group. The ad group will contain one ad if you have configured a carousel or several ads if you have added creatives as separate ads.
## Step 7. Enter payment details
To start running the ad, after you configure targeting and ad budget, enter your payment details. After that, you are all set!
## What's next
Now, you can [explore how the campaign affects your app revenue in the Adapty User Acquisition dashboard](adapty-user-acquisition.md).
Not using Adapty User Acquisition yet? [Book a call with us](https://calendly.com/tnurutdinov-adapty/30min) to learn how it can help you track and optimize your ad campaigns.
---
# File: getting-started-with-server-side-api
---
---
title: "Server-side API"
description: "Get started with Adapty's server-side API for subscription management."
---
With the API, you can:
1. Check a user's subscription status.
2. Activate a user's subscription with an access level.
3. Retrieve user attributes.
4. Set user attributes.
5. Get and update paywall configurations.
:::note To track subscription events, use [Webhook](webhook) integration in Adapty or integrate directly with your existing service. ::: ## Case 1: Sync subscribers between web and mobile If you use web payment providers like Stripe, ChargeBee, or others, you can sync your subscribers easily. Here's how: 1.
Adapty is a versatile platform designed to help mobile apps grow. Whether you’re just starting out or already have thousands of users, Adapty lets you save months on integrating in-app purchases and double subscription revenue with paywall management.
The Adapty plugin for FlutterFlow lets you leverage all of Adapty’s features without any coding. You can design paywall pages in FlutterFlow, enable purchases for them, and then remotely control which products get displayed on them, including targeting to specific user groups or A/B testing. And after you release your app, you can instantly access detailed analytics of your customers' purchases right in our dashboard.
Want to update the products available on your paywall? It’s simple! Make changes in just a few clicks within the Adapty Dashboard, and your customers will see the new products immediately — no need to release a new app version!
What else Adapty offers you:
- **Subscriptions and in-App Purchases**: Adapty handles server-side receipt validation for you and syncs your customers across all platforms, including the web.
- **A/B Testing for paywalls**: Test different prices, durations, trial periods, and visual elements to optimize your subscription and one-time offerings.
- **Powerful analytics**: Access detailed metrics to better understand and improve your app’s monetization.
- **Integrations**: Adapty seamlessly connects with third-party analytics tools like Amplitude, AppsFlyer, Adjust, Branch, Mixpanel, Facebook Ads, AppMetrica, custom Webhooks, and more.
---
# File: ff-getting-started
---
---
title: "Getting started"
description: "Get started with Adapty Feature Flags to personalize subscription flows."
---
With Adapty, you can create and run paywalls and A/B tests at different points in your mobile app user's journey, such as Onboarding, Settings, etc. These points are called [Placements](placements). A placement in your app can manage multiple paywalls or [A/B tests](ab-tests) at a time, each made for a certain group of users, which we call [Audiences](audience). Moreover, you can experiment with paywalls, replacing one with another over time without releasing a new app version. The only thing you hardcode in the mobile app is the placement ID.
The Adapty library keeps your paywall updated with the latest products from your Adapty Dashboard. It [fetches the product data](ff-action-flow) and [shows it on your paywall](ff-add-variables-to-paywalls), [handles purchases](ff-make-purchase), and [checks the user’s access level](ff-check-subscription-status) to see if they should get paid content.
To get started, just [add the Adapty library](ff-getting-started#add-the-adapty-plugin-as-a-dependency) to your FlutterFlow project and [initiate it](ff-getting-started#initiate-adapty-plugin) as shown below.
:::warning
Before you start, note the following limitations:
- The Adapty library for FlutterFlow doesn’t support web apps. Avoid compiling web apps with it.
- The Adapty library for FlutterFlow doesn't support paywalls creating using the Adapty paywall builder. You need to design your own paywall in FlutterFlow before enabling purchases with Adapty.
:::
## Add the Adapty library as a dependency
1. In the [FlutterFlow Dashboard](https://app.flutterflow.io/dashboard), open your project, and then click **Settings and Integrations** from the left menu. In the **Project setup** section on the left, select **Project dependencies**.
2. In the **FlutterFlow Libraries** section, click **Add Library** and enter `adapty-xtuel0`. Click **Add**.
3. Now, you need to associate your SDK key with the library. Click **View details** next to the library.
4. Copy the **Public SDK key** from the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard.
5. Paste the key to **AdaptyApiKey** in FlutterFlow.
The Adapty FF library will now be added as a dependency to your project. In the **Adapty** FF library window, you’ll find all the Adapty resources that have been imported into your project.
## Call the new activation action at application launch
1. Go to **Custom Code** section from the left menu and open `main.dart`.
2. Click **+** and select `activate (Adapty)`.
3. Click **Save**.
## Initiate Adapty plugin
For the Adapty Dashboard to recognize your app, you’ll need to provide a special key in FlutterFlow.
1. In your FlutterFlow project, go to **Settings and Integrations > Permissions** from the left menu.
2. In the opened **Permissions** window, click the **Add Permission** button.
3. In both the **iOS Permission Key** and **Android Permission Key** field, paste `AdaptyPublicSdkKey`.
4. For the **Permission Message**, copy the **Public SDK key** from the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard. Each app has its own SDK key, so if you have multiple apps, make sure you grab the right one.
After completing these steps, you'll be able to call your paywall in your FlutterFlow app and enable purchases through it.
## What's next?
1. [Create an action flow](ff-action-flow) for handling Adapty paywall products and their data in FlutterFlow.
2. [Map the received data to the paywall](ff-add-variables-to-paywalls) you designed in FlutterFlow.
3. [Set up the purchase button](ff-make-purchase) on your paywall to process transactions through Adapty when clicked.
4. Finally, [add subscription status checks](ff-check-subscription-status) to determine whether to display paid content to the user.
---
# File: ff-action-flow
---
---
title: "Step 1. Create flow to show paywall data"
description: "Set up feature flag action flows in Adapty to personalize user subscription journeys."
---
:::important
When using the FlutterFlow plugin, you can't use paywalls created in the Adapty Paywall builder. You must implement your own paywall page in FlutterFlow and connect it to Adapty.
:::
After adding the Adapty library as a dependency to your FlutterFlow project, it's time to build the flow that **retrieves Adapty paywall and product data and displays it on the paywall you've designed in FlutterFlow**.
We first need to receive the paywall data from Adapty. We'll start by requesting the Adapty paywall, then its associated products, and finally checking if the data was successfully received. If successful, we’ll display the product title and price on the paywall page. Otherwise, we'll show an error message.
Before proceeding, make sure you've done the following:
1. [Created at least one paywall and added at least one product to it](create-paywall) in the Adapty Dashboard.
2. [Created at last one placement](create-placement) and [added your paywall to it](add-audience-paywall-ab-test) in the Adapty Dashboard.
Let's get started!
## Step 1.1. Request Adapty paywall
As mentioned, to display data in your FlutterFlow paywall, we first need to retrieve it from Adapty. The initial step is to get the Adapty paywall itself. Here’s how:
1. Open your paywall screen and switch to the **Actions** section in the right pane. There, open the **Action Flow Editor**.
2. In the **Select Action Trigger** window, select **On Page Load**.
3. Click **Add Action**. Then, search for the `getPaywall` custom action and select it.
4. In the **Set Actions Arguments** section, enter the real ID of the [placement you have created](create-placement) in the Adapty Dashboard that includes the paywall. In this example it's `monthly`. Be sure to use your real placement ID!
5. If you have [localized](localizations-and-locale-codes.md) your paywall in the Adapty dashboard, you can also set up the **locale** argument.
6. In the **Action Output Variable Name**, create a new variable and name it `getPaywallResult`. We'll use this in the next step to reference the Adapty paywall and request its products.
## Step 1.2. Request Adapty paywall products
Great! We’ve retrieved the Adapty paywall. Now, let's get the products associated with this paywall:
1. Click **+** under the created action and select **Add Action**. This action will receive Adapty paywall products. For this, search and select `getPaywallProducts`.
2. In the **Set Actions Arguments** section, select the `getPaywallResult` variable created earlier.
3. Fill in the other fields as follows:
- **Available Options**: Data Structured Field
- **Select Field**: value
- **Available Options**: No further changes
4. Click **Confirm**.
5. In the **Action Output Variable Name**, create a new variable and name it `getPaywallProductsResult`. We'll use this to map the paywall you designed in FlutterFlow with the Adapty paywall data.
## Step 1.3. Add check if the paywall uploaded successfully
Before moving on, let’s verify that the Adapty paywall was received successfully. If so, we can update the paywall with the product data. If not, we’ll handle the error. Here's how to add the check:
1. Click **+** and click **Add Conditional**.
2. In the **Action Output** section, select the action output variable created earlier (`getPaywallResult` in our example).
3. To verify that the Adapty paywall was received, check for the presence of a field with a value. Fill in the fields as follows:
- **Available Options**: Has Field
- **Field (AdaptyGetPaywallResult)**: value
4. Click **Confirm** to finalize the condition.
## Step 1.4. Log the paywall review
To ensure Adapty analytics track the paywall view, we need to log this event. Without this step, the view won’t be counted in the analytics. Here’s how:
1. Click **+** under the **TRUE** label and click **Add Action**.
2. In the **Select Action** field, search for and choose **logShowPaywall**.
3. Click **Value** in the **Set Action Arguments** area and choose the `getPaywallResult` variable we've created. This variable contains the paywall data.
4. Fill in the fields as follows:
- **Available Options**: Data Structured Field
- **Select Field**: value
5. Click **Confirm**.
## Step 1.5. Show error if paywall not received
If the Adapty paywall is not received, you need to [handle the error](error-handling-on-flutter-react-native-unity#system-storekit-codes). In this example, we'll simply display an alert message.
1. Add an **Informational Dialog** action to the **FALSE** label.
2. In the **Title** field, add text you want to see as the dialog title. In this example, it's **Error**.
3. Click **Value** in the **Message** box.
4. Fill in the fields as follows:
- **Set Variable**: `getPaywallProductResult` variable we've created
- **Available Options**: Data Structure Field
- **Select Field**: error
- **Available Options**: Data Structure Field
- **Select Field**: errorMessage
5. Click **Confirm**.
6. Add a **Terminate action** to the **FALSE** flow.
7. Click **Close** in the top-right corner.
Congratulations! You’ve successfully received the product data. Now, let’s [map it to your paywall you've designed in FlutterFlow](ff-add-variables-to-paywalls).
---
# File: ff-add-variables-to-paywalls
---
---
title: "Step 2. Add data to paywall page"
description: "Add Feature Flag variables to paywalls in Adapty."
---
Once you've [received all the necessary product data](ff-action-flow), it's time to map it to the beautiful paywall you designed in FlutterFlow. In this example, we'll map the product title and its price.
## Step 2.1. Add product title to paywall page
1. Double-click the product text on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it.
2. Fill in the fields as follows:
- **Available Options**: Data Structured Field
- **Select Field**: value
- **Available Options**: Item at Index
- **List Index Options**: First
- **Available Options**: Data Structured Field
- **Select Field**: localizedTitle
- **Default Variable Value**: null
- **UI Builder Display Value**: Anything, in the example, it's `product.title`
3. Click **Confirm** to save the changes.
## Step 2.2. Add price text to paywall page
Repeat the steps from Step 2.1 for the price text as shown below:
1. Double-click the price text on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it.
2. Fill in the fields as follows:
- **Available Options**: Data Structured Field
- **Select Field**: value
- **Available Options**: Item at Index
- **List Index Options**: First
- **Available Options**: Data Structured Field
- **Select Field**: price
- **Default Variable Value**: null
- **UI Builder Display Value**: Anything, in the example, it's `product.price`
3. Click the **Confirm** button to save the changes.
### Add price in local currency to paywall page
1. Double-click the price on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it.
2. Fill in the fields as follows:
- **Available Options**: Data Structured Field
- **Select Field**: value
- **Available Options**: Item at Index
- **List Index Options**: First
- **Available Options**: Data Structured Field
- **Select Field**: price
- **Available Options**: Data Structured Field
- **Select Field**: amount
- **Available Options**: Decimal
- **Decimal Type**: Automatic
- **Default Variable Value**: null
- **UI Builder Display Value**: Anything, in the example, it's `price.amount`
3. Click **Confirm** to save the changes.
And voilà! Now, when you launch your app, it will display the product data from the Adapty paywall directly on your paywall page!
It's time to [let your users purchase this product](ff-make-purchase).
---
# File: ff-make-purchase
---
---
title: "Step 3. Enable purchase"
description: "Learn how to make purchases using Adapty’s Feature Flags system."
---
Congratulations! You've successfully [set up your paywall to display product data from Adapty](ff-add-variables-to-paywalls), including the product title and price.
Now, let's move on to the final step – letting users make a purchase through the paywall.
## Step 3.1. Enable users to make purchases
1. Double-click the buy button on your paywall page. In the right panel, open the **Actions** section if it's not already open.
2. Open the **Action Flow Editor**.
3. In the **Select Action Trigger** window, choose **On Tap**.
4. In the **No Actions Created** window, click **Add Action**. Search for the `makePurchase` action and choose it.
5. In the **Set Actions Arguments** section, choose `getPaywallProductsResult` variable created earlier.
6. Fill in the fields as follows:
- **Available Options**: Data Structure Field
- **Select Field**: value
- **Available Options**: Item at Index
- **List Index Options**: First
7. Click `subscriptionUpdateParameters`, search for `AdaptySubscriptionUpdateParameters` and select it. Click **Confirm**.
:::info
By default, you can leave all the object fields empty. You would need to fill them in to replace one subscription with another in Android apps. Read more [here](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/).
:::
8. Click **Confirm**.
9. In the **Action Output Variable Name**, create a new variable and name it `makePurchaseResult` - this will be used later to confirm the purchase was successful.
## Step 3.2. Check if the purchase was successful
Now, let's set up a check to see if the purchase went through.
1. Click **+** and click **Add Conditional**.
2. In **Set Condition for Action**, select the `makePurchaseResult` variable.
3. In the **Set Variable** window, fill in the fields as follows:
- **Available Options**: Has Field
- **Select Field**: profile
4. Click **Confirm**.
## Step 3.3. Open paid content
If the purchase is successful, you can unlock the paid content. Here’s how to set that up:
1. Click **+** under the **TRUE** label and click **Add Action**.
2. In the **Define Action** field, search for and select the page you want to open from the **Navigate To** list. In this example, the page is **Questions**.
## Step 3.4 Show error message if purchase failed
If the purchase fails, let's display an alert to the user.
1. Add an **Informational Dialog** action to the **FALSE** label.
2. In the **Title** field, enter the text you want for the dialog title, such as **Purchase Failed**.
3. Click **Value** in the **Message** box. In the **Set from Variable** window, search for `makePurchaseResult` and choose it. Fill in the fields as follows:
- **Available Options**: Data Structure Field
- **Select Field**: error
- **Available Options**: Data Structure Field
- **Select Field**: errorMessage
4. Click **Confirm**.
5. Add a **Terminate** action to the **FALSE** flow.
6. Finally, click **Close** in the top-right corner.
Congratulations! Your users can now purchase your products. As an extra step, let's [set up a check for user access to paid content](ff-check-subscription-status) elsewhere to decide whether to display paid content or the paywall to them.
---
# File: ff-check-subscription-status
---
---
title: "Step 4. Check access to paid content"
description: "Learn how to check subscription status using Adapty's feature flags for better user segmentation."
---
When determining if a user has access to specific paid content, you'll need to verify their access level. This means checking if the user has at least one access level, and if that level is the required one.
You can do this by checking the user profile, which contains all available access levels.
Now, let’s allow users to purchase your product:
1. Double-click the button that should show the paid content and open the **Actions** section in the right pane if it’s not already open.
2. Open the **Action Flow Editor**.
3. In the **Select Action Trigger** window, choose **On Tap**.
4. In the **No Actions Created** window, click the **Add Conditional Action** button.
5. Click **UNSET** to set action arguments and choose the `currentProfile` variable. This is the Adapty variable that holds data about the current user's profile.
6. Fill in the fields as follows:
- **Available Options**: Data Structure Field
- **Select Field**: accessLevels
- **Available Options**: Filter List Items
- **Filter Conditions**:
1. Select **Conditions -> Single Condition** and click **UNSET**.
2. In the **First value** field, select **Item in list** as **Source** and fill in the fields as follows:
- **Available Options**: Data Structure Field
- **Select Field**: accessLevelIdentifier
3. Set the filter operator to **Equal to**.
4. Click **UNSET** next to **Second value** and in the **Value** field, enter the ID of your access level; in our example we use `premium`.
5. Click **Confirm** and continue filling in the other fields below.
- **Available Options**: Item at Index
- **List Index Options**: First
- **Available Options**: Data Structure Field
- **Select Field**: accessLevel
- **Available Options**: Data Structure Field
- **Select Field**: isActive
7. Click **Confirm**.
Now, add the actions for what happens next — if the user has the right subscription or not. Either take them to the page available to premium subscribers or open the paywall page so they can buy access.
---
# File: ff-resources
---
---
title: "Adapty FlutterFlow plugin actions and data types"
description: "Access Adapty's feature flag resources to streamline subscription-based features."
---
## Custom Actions
Below are Adapty methods delivered to FlutterFlow with Adapty plugin. They can be used as custom actions in FlutterFlow.
| Custom Action | Description | Action Arguments | Adapty Data Types - Action Output Variable |
|---|----|--------|----|
| activate | Initializes the Adapty SDK | None ||
| getPaywall
| Retrieves a paywall. It does not return paywall products. Use the `getPaywallProducts` action to get the actual products |getPaywallProducts
| Returns a list of actual paywall products | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyGetProductsResult](ff-resources#adaptygetproductsresult) | |getProductsIntroductoryOfferEligibility
| Checks if the user qualifies for an introductory iOS subscription offer | [AdaptyPaywallProduct](product) | [AdaptyGetIntroEligibilitiesResult](ff-resources#adaptygetintroeligibilitiesresult) | |makePurchase
| Completes a purchase and unlocks content. If a paywall has a promotional offer, Adapty automatically applies it at checkout|getProfile
|Retrieves the current app user's profile. This allows you to set access levels and other parameters
If it fails (e.g., due to no internet), cached data will be returned. Adapty regularly updates the profile cache to ensure the information stays as current as possible
| None| [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | updateProfile | Changes optional attributes of the current user profile such as email, phone number, etc. You can later use attributes to create user [segments](segments) or just view them in CRM | The ID and any parameters that need to be updated for the [AdaptyProfile](ff-resources#adaptyprofile) | [AdaptyError](ff-resources#adaptyerror) (Optional) | | restorePurchases | Restores any purchases the user has made | None | [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | logShowPaywall | Logs when a specific paywall is shown to the user | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyError](ff-resources#adaptyerror) (Optional) | | identify | Identifies the user using your system's `customerUserId` | customerUserId | [AdaptyError](ff-resources#adaptyerror) (Optional) | | logout | Logs the current user out of your app | None | [AdaptyError](ff-resources#adaptyerror) (Optional)| | presentCodeRedemptionSheet | Displays a sheet that allows users to redeem codes (iOS only) | None | None | ## Data Types Adapty data types (collections of data values) delivered to FlutterFlow with Adapty plugin. ### AdaptyAccessLevel Information about the user's [access level](access-level). | Field Name | Type | Description | |--------------------------|----------|-------------| | activatedAt | DateTime | The time when this access level was activated | | activeIntroductoryOfferType | String | The type of an active introductory offer. If set, it means an offer was applied during this subscription period | | activePromotionalOfferId | String | The ID of an active promotional offer (purchased from iOS) | | activePromotionalOfferType | String | The type of an active promotional offer (purchased from iOS). If set, it means an offer was applied during this subscription period | | billingIssueDetectedAt | DateTime | The time when a billing issue was detected. The subscription can still be active. Set to null if payment is successfully processed | | cancellationReason | String | The reason why the subscription was canceled | | expiresAt | DateTime | The access level expiration time (could be in the past or not set for lifetime access) | | id | String | The identifier of the access level | | isActive | Boolean | True if this access level is active. Generally, you can check this property to determine if a user has an access to premium features | | isInGracePeriod | Boolean | True if this auto-renewable subscription is in the [grace period](https://developer.apple.com/help/app-store-connect/manage-subscriptions/enable-billing-grace-period-for-auto-renewable-subscriptions) | | isLifetime | Boolean | True if this access level is active for a lifetime (no expiration date) | | isRefund | Boolean | True if this purchase was refunded | | offerId | String | The ID of an active promotional offer (purchased from Android) | | renewedAt | DateTime | The time when the access level was last renewed | | startsAt | DateTime | The start time of this access level (could be in the future) | | store | String | The store where the purchase was made | | unsubscribedAt | DateTime | The time when auto-renewal was turned off for the subscription. The subscription can still be active. If not set, the user reactivated the subscription | | vendorProductId | String | The product ID from the store that unlocked this access level | | willRenew | Boolean | True if this auto-renewable subscription is set to renew | ### AdaptyAccessLevelIdentifiers This struct is intended to replace key-value pair for `Map
2. Download and import the [External Dependency Manager plugin](https://github.com/googlesamples/unity-jar-resolver).
3. The SDK uses the "External Dependency Manager" plugin to handle iOS Cocoapods dependencies and Android gradle dependencies. After the installation, you may need to invoke the dependency manager:
`Assets -> External Dependency Manager -> Android Resolver -> Force Resolve`
and
`Assets -> External Dependency Manager -> iOS Resolver -> Install Cocoapods`
4. When building your Unity project for iOS, you would get `Unity-iPhone.xcworkspace` file, which you have to open instead of `Unity-iPhone.xcodeproj`, otherwise, Cocoapods dependencies won't be used.
## Activate Adapty module of Adapty SDK
Activate the Adapty SDK in your app code.
:::note
The Adapty SDK only needs to be activated once in your app.
:::
To get your **Public SDK Key**:
1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general).
2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key).
3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code.
:::important
- Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only.
- **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.
:::
```csharp showLineNumbers title="C#"
using UnityEngine;
using AdaptySDK;
public class AdaptyListener : MonoBehaviour, AdaptyEventListener {
void Start() {
DontDestroyOnLoad(this.gameObject);
Adapty.SetEventListener(this);
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY");
Adapty.Activate(builder.Build(), (error) => {
if (error != null) {
// handle the error
return;
}
});
}
public void OnLoadLatestProfile(AdaptyProfile profile) { }
public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { }
public void OnInstallationDetailsFail(AdaptyError error) { }
}
```
## Set up event listening
Create a script to listen to Adapty events. Name it `AdaptyListener` in your scene. We suggest using the `DontDestroyOnLoad` method for this object to ensure it persists throughout the application's lifespan.
Adapty uses `AdaptySDK` namespace. At the top of your script files that use the Adapty SDK, you may add:
```csharp showLineNumbers title="C#"
using AdaptySDK;
```
Subscribe to Adapty events:
```csharp showLineNumbers title="C#"
using UnityEngine;
using AdaptySDK;
public class AdaptyListener : MonoBehaviour, AdaptyEventListener {
public void OnLoadLatestProfile(AdaptyProfile profile) {
// handle updated profile data
}
public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { }
public void OnInstallationDetailsFail(AdaptyError error) { }
}
```
We recommend adjusting the Script Execution Order to place the AdaptyListener before Default Time. This ensures that Adapty initializes as early as possible.
## Add Kotlin Plugin to your project
:::warning
This step is required. If you skip it, your mobile app can crash when the paywall is displayed.
:::
1. In **Player Settings**, ensure that the **Custom Launcher Gradle Template** and **Custom Base Gradle Template** options are selected.
2. Add the following line to `/Assets/Plugins/Android/launcherTemplate.gradle`:
```groovy showLineNumbers
apply plugin: 'com.android.application'
// highlight-next-line
apply plugin: 'kotlin-android'
apply from: 'setupSymbols.gradle'
apply from: '../shared/keepUnitySymbols.gradle'
```
3. Add the following line to `/Assets/Plugins/Android/baseProjectTemplate.gradle`:
```groovy showLineNumbers
plugins {
// If you are changing the Android Gradle Plugin version, make sure it is compatible with the Gradle version preinstalled with Unity
// See which Gradle version is preinstalled with Unity here https://docs.unity3d.com/Manual/android-gradle-overview.html
// See official Gradle and Android Gradle Plugin compatibility table here https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
// To specify a custom Gradle version in Unity, go do "Preferences > External Tools", uncheck "Gradle Installed with Unity (recommended)" and specify a path to a custom Gradle version
id 'com.android.application' version '8.3.0' apply false
id 'com.android.library' version '8.3.0' apply false
// highlight-next-line
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
**BUILD_SCRIPT_DEPS**
}
```
Now set up paywalls in your app:
- If you use [Adapty Paywall Builder](adapty-paywall-builder), first [activate the AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) below, then follow the [Paywall Builder quickstart](unity-quickstart-paywalls).
- If you build your own paywall UI, see the [quickstart for custom paywalls](unity-quickstart-manual).
## Activate AdaptyUI module of Adapty SDK
If you plan to use [Paywall Builder](adapty-paywall-builder.md) and have installed AdaptyUI module, you need AdaptyUI to be active. You can activate it during the configuration:
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetActivateUI(true);
```
## Optional setup
### Logging
#### Set up the logging system
Adapty logs errors and other important information to help you understand what is going on. There are the following levels available:
| Level | Description |
| ---------- | ------------------------------------------------------------ |
| `error` | Only errors will be logged |
| `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged |
| `info` | Errors, warnings, and various information messages will be logged |
| `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged |
You can set the log level in your app during Adapty configuration:
```csharp showLineNumbers title="C#"
// 'verbose' is recommended for development and the first production release
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY");
builder.LogLevel = AdaptyLogLevel.Verbose;
```
You can also change the log level at runtime:
```csharp showLineNumbers title="C#"
Adapty.SetLogLevel(AdaptyLogLevel.Verbose, (error) => {
// handle result
});
```
### Data policies
Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines.
#### Disable IP address collection and sharing
When activating the Adapty module, set `SetIPAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`.
Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app.
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetIPAddressCollectionDisabled(true);
```
#### Disable advertising ID collection and sharing
When activating the Adapty module, set `SetAppleIDFACollectionDisabled` and/or `SetGoogleAdvertisingIdCollectionDisabled` to `true` to disable the collection of advertising identifiers. The default value is `false`.
Use this parameter to comply with App Store/Google Play policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs.
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetAppleIDFACollectionDisabled(true)
.SetGoogleAdvertisingIdCollectionDisabled(true);
```
#### Set up media cache configuration for AdaptyUI
By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration.
Use `SetAdaptyUIMediaCache` to override the default cache settings:
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetAdaptyUIMediaCache(
100 * 1024 * 1024, // MemoryStorageTotalCostLimit 100MB
null, // MemoryStorageCountLimit
100 * 1024 * 1024 // DiskStorageSizeLimit 100MB
);
```
Parameters:
| Parameter | Required | Description |
|-----------------------------|----------|----------------------------------------------------------------------------------|
| memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Defaults to platform-specific value. |
| memoryStorageCountLimit | optional | The item count limit of the memory storage. Defaults to platform-specific value. |
| diskStorageSizeLimit | optional | The file size limit on disk in bytes. Defaults to platform-specific value. |
### Enable local access levels (Android)
By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `SetGoogleLocalAccessLevelAllowed` to `true`:
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetGoogleLocalAccessLevelAllowed(true);
```
### Clear data on backup restore
When `SetAppleClearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`.
:::note
Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged.
:::
```csharp showLineNumbers title="C#"
var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY")
.SetAppleClearDataOnBackup(true);
```
## Troubleshooting
#### Android backup rules (Auto Backup configuration)
Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`.
Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules)
is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)`
:::note
These changes should be made in your Android platform directory (typically located in your project's `android/` folder).
:::
To resolve this, you need to:
- Tell the manifest merger to use your app's values for backup-related attributes.
- Create backup rule files that merge Adapty's rules with rules from other SDKs.
#### 1. Add the `tools` namespace to your manifest
In your `AndroidManifest.xml` file, ensure the root `
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```csharp showLineNumbers
Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user
if(error == null) {
// successful identify
}
});
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```csharp showLineNumbers
using UnityEngine;
using AdaptySDK;
var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY")
.SetCustomerUserId("YOUR_USER_ID"); // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
Adapty.Activate(builder.Build(), (error) => {
if (error != null) {
// handle the error
return;
}
});
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```csharp showLineNumbers
Adapty.Logout((error) => {
if(error == null) {
// successful logout
}
});
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](unity-check-subscription-status.md) right after the identification, or [listen for profile updates](unity-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected
- [**Onboardings**](onboardings.md): Engage users with onboardings and drive retention
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](unity-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor-unity
---
---
title: "Integrate Adapty into your Unity app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your Unity app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your Unity app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-unity.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Unity app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration builder.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.GetPaywall("YOUR_PLACEMENT_ID")`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.AccessLevels["premium"]?.IsActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `GetPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the Unity SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-unity.md](https://adapty.io/docs/adapty-cursor-unity.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](unity-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](unity-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK package via Unity Package Manager and activate it with your Public SDK key. This is the foundation — nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-unity.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-unity.md
```
:::tip[Checkpoint]
- **Expected:** Project builds and runs. Unity Console shows Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-unity). In Unity SDK, directly call the `CreatePaywallView` method without manually fetching the view configuration first. :::warning The result of the `CreatePaywallView` method can only be used once. If you need to use it again, call the `CreatePaywallView` method anew. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers var parameters = new AdaptyUICreatePaywallViewParameters() .SetPreloadProducts(preloadProducts) .SetLoadTimeout(new TimeSpan(0, 0, 3)); AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { // handle the result }); ``` Parameters: | Parameter | Presence | Description | | :------------------ | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. | | **PreloadProducts** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **CustomTags** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. | | **CustomTimers** | optional | Define a dictionary of custom timers and their end dates. Custom timers allow you to display countdown timers in your paywall. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](localizations-and-locale-codes). ::: Once you have the view, [present the paywall](unity-present-paywalls). ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Unity SDK to version 3.8.0 or higher. ::: Here's an example of how you can provide custom assets via a simple dictionary: ```csharp showLineNumbers var customAssets = new Dictionaryoptional
default: `en`
|The identifier of the paywall localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and fallback paywalls. We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| --- # File: unity-present-paywalls --- --- title: "Display paywalls" description: "Learn how to display paywalls in your Unity app with Adapty SDK." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide covers the **new Paywall Builder**, which requires Adapty SDK 3.3.0 or later. If you're using the legacy Paywall Builder (compatible with Adapty SDK version 2.x and earlier), check out [Present legacy Paywall Builder paywalls in Unity](unity-present-paywalls-legacy). To present remote config paywalls, see [Render paywalls designed with remote config](present-remote-config-paywalls). ::: To display a paywall, use the `view.Present()` method on the `view` created by the [`CreatePaywallView`](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `CreatePaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Show dialog Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms. ```csharp showLineNumbers title="Unity" var dialog = new AdaptyUIDialogConfiguration() .SetTitle("Close paywall?") .SetContent("You will lose access to exclusive offers.") .SetDefaultActionTitle("Stay") .SetSecondaryActionTitle("Close"); AdaptyUI.ShowDialog(view, dialog, (action, error) => { if (error == null) { if (action == AdaptyUIDialogActionType.Secondary) { // User confirmed - close the paywall view.Dismiss(); } // If primary - do nothing, user stays } }); ``` ## Configure iOS presentation style Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `Present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FullScreen` (default) or `AdaptyUIIOSPresentationStyle.PageSheet` values. ```csharp showLineNumbers title="Unity" view.Present(AdaptyUIIOSPresentationStyle.PageSheet, (error) => { // handle the error }); ``` --- # File: unity-handle-paywall-actions --- --- title: "Respond to button actions in Unity SDK" description: "Handle paywall button actions in Unity using Adapty for better app monetization." --- If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. :::warning **Only purchases and restorations are handled automatically.** All the other button actions, such as closing paywalls or opening links, require implementing proper responses in the app code. ::: ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `close` action that dismisses the paywall. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: view.Dismiss(null); break; default: // handle other events break; } } ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.OpenUrl: var urlString = action.Value; if(!string.IsNullOrWhiteSpace(urlString)) { Application.OpenURL(urlString); } break; default: // handle other events break; } } ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it the **Custom** action with ID `login`. 2. In your app code, implement a handler for the `login` custom action that identifies your user. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Custom: if (action.Value == "login") { // Navigate to login scene SceneManager.LoadScene("LoginScene"); } break; default: // handle other events break; } } ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Custom: if (action.Value == "openNewPaywall") { // Display another paywall ShowAlternativePaywall(); } break; default: // handle other events break; } } private void ShowAlternativePaywall() { // Implement your logic to show alternative paywall } ``` --- # File: unity-handling-events --- --- title: "Handle paywall events" description: "Learn how to handle paywall events in your Unity app with Adapty SDK." --- :::important This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](unity-handle-paywall-actions.md) for details. ::: Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.3.0 or later. For presenting paywalls in Adapty SDK v2 designed with legacy Paywall Builder, see [Handle paywall events designed with legacy Paywall Builder](unity-handling-events-legacy). ::: :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Handling events To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallsEventsListener` interface: ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; public class PaywallEventsHandler : MonoBehaviour, AdaptyPaywallsEventsListener { void Start() { Adapty.SetPaywallsEventsListener(this); } // Implement all required interface methods below } ``` ### User-generated events #### Paywall appeared Invoked when the paywall view is presented on the screen. :::note On iOS, also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. ::: ```csharp showLineNumbers title="Unity" public void PaywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall disappeared Invoked when the paywall view is dismissed from the screen. :::note On iOS, also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. ::: ```csharp showLineNumbers title="Unity" public void PaywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Product selection Invoked when a product is selected for purchase (by a user or by the system). ```csharp showLineNumbers title="Unity" public void PaywallViewDidSelectProduct( AdaptyUIPaywallView view, string productId ) { } ```
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `LogShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `LogShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](unity-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: unity-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in Unity SDK"
description: "Integrate Adapty SDK into your custom Unity paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](unity-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](unity-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
```csharp showLineNumbers
using AdaptySDK;
void LoadPaywall() {
Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => {
if (error != null) {
// Handle the error
return;
}
Adapty.GetPaywallProducts(paywall, (products, productsError) => {
if (productsError != null) {
// Handle the error
return;
}
// Use products to build your custom paywall UI
});
});
}
```
## Step 2. Accept purchases
When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile.
```csharp showLineNumbers
using AdaptySDK;
void PurchaseProduct(AdaptyPaywallProduct product) {
Adapty.MakePurchase(product, (result, error) => {
if (error != null) {
// Handle the error
return;
}
switch (result.Type) {
case AdaptyPurchaseResultType.Success:
var profile = result.Profile;
// Purchase successful, profile updated
break;
case AdaptyPurchaseResultType.UserCancelled:
// User canceled the purchase
break;
case AdaptyPurchaseResultType.Pending:
// Purchase is pending (e.g., user will pay offline with cash)
break;
}
});
}
```
## Step 3. Restore purchases
App stores require all apps with subscriptions to provide a way users can restore their purchases.
Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile.
```csharp showLineNumbers
using AdaptySDK;
void RestorePurchases() {
Adapty.RestorePurchases((profile, error) => {
if (error != null) {
// Handle the error
return;
}
// Restore successful, profile updated
});
}
```
## Next steps
optional
default: `en`
|The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](unity-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](unity-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```csharp showLineNumbers Adapty.GetPaywallProducts(paywall, (products, error) => { if(error != null) { // handle the error return; } // products - the requested products array }); ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.LocalizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.Price.LocalizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.Price.Amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.Price.CurrencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.Subscription?.LocalizedPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.Subscription?.Period`. From there you can access the `Unit` enum to get the length (i.e. `AdaptySubscriptionPeriodUnit.Day`, `AdaptySubscriptionPeriodUnit.Week`, `AdaptySubscriptionPeriodUnit.Month`, `AdaptySubscriptionPeriodUnit.Year`, or `AdaptySubscriptionPeriodUnit.Unknown`). The `NumberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `AdaptySubscriptionPeriodUnit.Month` in the Unit property, and `3` in the NumberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.Subscription?.Offer?.Phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:optional
default: `en`
|The identifier of the paywall localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and fallback paywalls. We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
| --- # File: present-remote-config-paywalls-unity --- --- title: "Render paywall designed by remote config in Unity SDK" description: "Discover how to present remote config paywalls in Adapty Unity SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if (error != null) { // handle the error return; } // Access remote config dictionary var dictionary = paywall.RemoteConfig?.Dictionary; var headerText = dictionary?["header_text"] as string; // Or access raw JSON data var jsonData = paywall.RemoteConfig?.Data; }); ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-unity#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.MakePurchase()` with the product from your paywall. For details on the`.MakePurchase()` method, read [Making purchases](unity-making-purchases). We recommend [creating a backup paywall called a fallback paywall](unity-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.LogShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.LogShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```csharp showLineNumbers Adapty.LogShowPaywall(paywall, (error) => { // handle the error }); ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) object. | --- # File: unity-making-purchases --- --- title: "Make purchases in mobile app in Unity SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-unity#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](unity-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```csharp showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { Adapty.MakePurchase(product, (result, error) => { switch (result.Type) { case AdaptyPurchaseResultType.Pending: // handle pending purchase break; case AdaptyPurchaseResultType.UserCancelled: // handle purchase cancellation break; case AdaptyPurchaseResultType.Success: var profile = result.Profile; // handle successfull purchase break; default: break; } }); } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:------------------------------------------------------------------------------------------------------| | **Product** | required | An [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |If the request has been successful, the response contains this object. An [AdaptyProfile](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.html) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```csharp showLineNumbers // Create subscription update parameters var subscriptionUpdateParams = new AdaptySubscriptionUpdateParameters( "old_product_id", // Product ID of the current subscription AdaptySubscriptionUpdateReplacementMode.WithTimeProration ); Adapty.MakePurchase(product, subscriptionUpdateParams, (profile, error) => { if(error != null) { // Handle the error return; } // successful cross-grade }); ``` Additional request parameter: | Parameter | Presence | Description | | :--------------------------- | :------- |:-------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | required | an [`AdaptySubscriptionUpdateParameters`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_subscription_update_parameters.html) object. | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the SDK method: ```csharp showLineNumbers Adapty.PresentCodeRedemptionSheet((error) => { // handle the error }); ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ## Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetGoogleEnablePendingPrepaidPlans(true); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` --- # File: unity-restore-purchase --- --- title: "Restore purchases in mobile app in Unity SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```csharp showLineNumbers Adapty.RestorePurchases((profile, error) => { if (error != null) { // handle the error return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel != null && accessLevel.IsActive) { // restore access } }); ``` Response parameters: | Parameter | Description | |---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |An [`AdaptyProfile`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.html) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-unity --- --- title: "Implement Observer mode in Unity SDK" description: "Implement observer mode in Adapty to track user subscription events in Unity SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Unity](sdk-installation-unity#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-unity) from your existing purchase infrastructure to Adapty. ### Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { void Start() { DontDestroyOnLoad(this.gameObject); Adapty.SetEventListener(this); var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetObserverMode(true); // Enable observer mode Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); } public void OnLoadLatestProfile(AdaptyProfile profile) { } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } } ``` Parameters: | Parameter | Description | |--------------|-----------------------------------------------------------------------------------------------------| | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-unity.md). 3. [Associate paywalls](report-transactions-observer-mode-unity) with purchase transactions. --- # File: report-transactions-observer-mode-unity --- --- title: "Report transactions in Observer Mode in Unity SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Unity SDK." ---For iOS, StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) object. |phoneNumber
firstName
lastName
| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```csharp showLineNumbers try { builder = builder.SetCustomStringAttribute("string_key", "string_value"); builder = builder.SetCustomDoubleAttribute("double_key", 123.0f); } catch (Exception e) { // handle the exception } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```csharp showLineNumbers try { builder = builder.RemoveCustomAttribute("key_to_remove"); } catch (Exception e) { // handle the exception } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: unity-listen-subscription-changes --- --- title: "Check subscription status in Unity SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Unity app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).An [AdaptyProfile](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.html) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // "premium" is an identifier of default access level var accessLevel = profile.AccessLevels["premium"]; if (accessLevel != null && accessLevel.IsActive) { // grant access to premium features } }); ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```csharp showLineNumbers // Extend `AdaptyEventListener ` with `OnLoadLatestProfile ` method: public class AdaptyListener : MonoBehaviour, AdaptyEventListener { public void OnLoadLatestProfile(AdaptyProfile profile) { // handle any changes to subscription state } } ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: unity-deal-with-att --- --- title: "Deal with ATT in Unity SDK" description: "Get started with Adapty on Unity to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```csharp showLineNumbers var builder = new Adapty.ProfileParameters.Builder() .SetAppTrackingTransparencyStatus(IOSAppTrackingTransparencyStatus.Authorized); Adapty.UpdateProfile(builder.Build(), (error) => { if(error != null) { // handle the error } }); ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-unity --- --- title: "Kids Mode in Unity SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in Unity SDK." --- If your Unity application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/) and [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| | **loadTimeout** | default: 5 sec |This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
| Response parameters: | Parameter | Description | |:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_onboarding.html) object with: the onboarding identifier and configuration, remote config, and several other properties. | After fetching the onboarding, call the `CreateOnboardingView` method. :::warning The result of the `CreateOnboardingView` method can only be used once. If you need to use it again, call the `CreateOnboardingView` method anew. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers AdaptyUI.CreateOnboardingView(onboarding, (view, error) => { // handle the result }); ``` Parameters: | Parameter | Presence | Description | |:---------------| :------------- |:-----------------------------------------------------------------------------| | **onboarding** | required | An `AdaptyOnboarding` object to obtain a view for the desired onboarding. | | **externalUrlsPresentation** |optional
default: `InAppBrowser`
|Controls how links in the onboarding are opened. Available options:
- `AdaptyWebPresentation.InAppBrowser` - Opens links in an in-app browser (default)
- `AdaptyWebPresentation.ExternalBrowser` - Opens links in the device's external browser
See [Customize how links open in onboardings](unity-present-onboardings#customize-how-links-open-in-onboardings) for usage examples.
| Once you have successfully loaded the onboarding and its view configuration, you can [present it in your mobile app](unity-present-onboardings). ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `GetOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `GetOnboarding` instead of `GetOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `GetOnboardingForDefaultAudience` as shown below. Otherwise, use `GetOnboarding` as described [above](#fetch-onboarding). ::: ```csharp showLineNumbers Adapty.GetOnboardingForDefaultAudience("YOUR_PLACEMENT_ID", (onboarding, error) => { if (error != null) { // handle the error return; } // the requested onboarding }); ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |optional
default: `en`
|The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
| --- # File: unity-present-onboardings --- --- title: "Present onboardings in Unity SDK" description: "Learn how to present onboardings effectively to drive more conversions." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Unity app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty Unity SDK](sdk-installation-unity.md) 3.14.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). To display an onboarding, use the `view.Present()` method on the `view` created by the `CreateOnboardingView` method. Each `view` can only be used once. If you need to display the paywall again, call `CreateOnboardingView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers title="Unity" view.Present((presentError) => { if (presentError != null) { // handle the error } }; ``` ## Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `Present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FullScreen` (default) or `AdaptyUIIOSPresentationStyle.PageSheet` values. ```csharp showLineNumbers title="Unity" view.Present(AdaptyUIIOSPresentationStyle.PageSheet, (error) => { // handle the error }); ``` ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v. 3.15. ::: By default, links in onboardings open in an in-app browser, providing a seamless experience by displaying web pages within your application without switching apps. To open links in an external browser instead, pass `AdaptyWebPresentation.ExternalBrowser` to the `CreateOnboardingView` method: ```csharp showLineNumbers title="Unity" AdaptyUI.CreateOnboardingView( onboarding, AdaptyWebPresentation.ExternalBrowser, // default — InAppBrowser (view, error) => { if (error != null) { // handle the error return; } // present the onboarding view view.Present((presentError) => { if (presentError != null) { // handle the error } }); } ); ``` Available options: - `AdaptyWebPresentation.InAppBrowser` - Opens links in an in-app browser (default) - `AdaptyWebPresentation.ExternalBrowser` - Opens links in the device's external browser --- # File: unity-handling-onboarding-events --- --- title: "Handle onboarding events in Unity SDK" description: "Handle onboarding-related events in Unity using Adapty." --- Before you start, ensure that: 1. You have installed [Adapty Unity SDK](sdk-installation-unity.md) 3.14.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below. To control or monitor processes occurring on the onboarding screen within your Unity app, implement the `AdaptyOnboardingsEventsListener` interface. ## Custom actions In the builder, you can add a **custom** action to a button and assign it an ID.
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the method `OnboardingViewOnCustomAction` will be triggered with the `actionId` parameter being the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
To handle onboarding events, implement the `AdaptyOnboardingsEventsListener` interface:
```csharp showLineNumbers title="Unity"
public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener
{
void Start()
{
Adapty.SetOnboardingsEventsListener(this);
}
public void OnboardingViewOnCustomAction(
AdaptyUIOnboardingView view,
AdaptyUIOnboardingMeta meta,
string actionId
)
{
if (actionId == "allowNotifications") {
// request notification permissions
}
}
public void OnboardingViewDidFailWithError(
AdaptyUIOnboardingView view,
AdaptyError error
)
{
// handle errors
}
// Implement other required interface methods (see examples below)
}
```
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
Implement the `OnboardingViewOnCloseAction` method in your class:
```csharp showLineNumbers title="Unity"
public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener
{
public void OnboardingViewOnCloseAction(
AdaptyUIOnboardingView view,
AdaptyUIOnboardingMeta meta,
string actionId
)
{
view.Dismiss((error) => {
if (error != null) {
// handle the error
}
});
}
// ... other interface methods
}
```
2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you're testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments-unity
---
---
title: "Fix for Code-1003 cantMakePayment error in Unity SDK"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: migration-to-unity-sdk-314
---
---
title: "Migrate Adapty Unity SDK to v. 3.14"
description: "Migrate to Adapty Unity SDK v3.14 for better performance and new monetization features."
---
Adapty SDK 3.14.0 is a major release that brought some improvements that however may require some migration steps from you:
1. Separate event listener for paywall events.
2. Rename `AdaptyUI.CreateView` to `AdaptyUI.CreatePaywallView` and related methods.
3. Update the `MakePurchase` method to use `AdaptyPurchaseParameters` instead of individual parameters.
4. Replace `SetFallbackPaywalls` with `SetFallback` method.
5. Update paywall property access to use `AdaptyPlacement`.
6. Update remote config access to use `AdaptyRemoteConfig` object.
7. Replace `VendorProductIds` with `ProductIdentifiers` in the `AdaptyPaywall` model.
8. Update `GetPaywall` fetch policy to use `AdaptyFetchPolicy`.
## Separate event listener for paywall events
If you display paywalls designed with the [Paywall Builder](adapty-paywall-builder), paywall view events now use the dedicated `AdaptyPaywallsEventsListener` interface and `SetPaywallsEventsListener` method. The core `AdaptyEventListener` interface remains for profile updates and installation details.
```diff showLineNumbers
using UnityEngine;
using AdaptySDK;
public class AdaptyListener : MonoBehaviour,
- AdaptyEventListener {
+ AdaptyEventListener,
+ AdaptyPaywallsEventsListener {
void Start() {
Adapty.SetEventListener(this);
+ Adapty.SetPaywallsEventsListener(this);
}
// AdaptyEventListener methods
public void OnLoadLatestProfile(AdaptyProfile profile) { }
public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { }
public void OnInstallationDetailsFail(AdaptyError error) { }
+ // AdaptyPaywallsEventsListener methods
+ // Implement paywall event handlers here
}
```
[Learn more about handling paywall events](unity-handling-events).
## Rename view creation and presentation methods
The view creation and presentation methods have been renamed:
```diff showLineNumbers
using AdaptySDK;
- AdaptyUI.CreateView(paywall, parameters, (view, error) => {
+ AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => {
if (error != null) {
// handle the error
return;
}
- AdaptyUI.PresentView(view, (error) => {
+ AdaptyUI.PresentPaywallView(view, (error) => {
// handle the error
});
});
}
```
Similarly, the dismiss method has been renamed:
```diff showLineNumbers
- AdaptyUI.DismissView(view, (error) => {
+ AdaptyUI.DismissPaywallView(view, (error) => {
// handle the error
});
```
## Update MakePurchase method
The `MakePurchase` method now uses `AdaptyPurchaseParameters` instead of individual `subscriptionUpdateParams` and `isOfferPersonalized` arguments. This provides better type safety and allows for future extensibility of purchase parameters.
```diff showLineNumbers
using AdaptySDK;
void MakePurchase(
AdaptyPaywallProduct product,
AdaptySubscriptionUpdateParameters subscriptionUpdate,
bool? isOfferPersonalized
) {
- Adapty.MakePurchase(product, subscriptionUpdate, isOfferPersonalized, (result, error) => {
+ var parameters = new AdaptyPurchaseParametersBuilder()
+ .SetSubscriptionUpdateParams(subscriptionUpdate)
+ .SetIsOfferPersonalized(isOfferPersonalized)
+ .Build();
+
+ Adapty.MakePurchase(product, parameters, (result, error) => {
switch (result.Type) {
case AdaptyPurchaseResultType.Pending:
// handle pending purchase
break;
case AdaptyPurchaseResultType.UserCancelled:
// handle purchase cancellation
break;
case AdaptyPurchaseResultType.Success:
var profile = result.Profile;
// handle successful purchase
break;
default:
break;
}
});
}
```
If no additional parameters are needed, you can simply use:
```csharp showLineNumbers
using AdaptySDK;
void MakePurchase(AdaptyPaywallProduct product) {
Adapty.MakePurchase(product, (result, error) => {
// handle purchase result
});
}
```
## Update fallback method
:::important
When upgrading to Unity SDK 3.14, you’ll need to download the new fallback files from the Adapty dashboard and replace the existing ones in your project.
:::
The method for setting fallbacks has been updated. The `SetFallbackPaywalls` method has been renamed to `SetFallback`:
```diff showLineNumbers
using AdaptySDK;
void SetFallBackPaywalls() {
#if UNITY_IOS
var assetId = "adapty_fallback_ios.json";
#elif UNITY_ANDROID
var assetId = "adapty_fallback_android.json";
#else
var assetId = "";
#endif
- Adapty.SetFallbackPaywalls(assetId, (error) => {
+ Adapty.SetFallback(assetId, (error) => {
// handle the error
});
}
```
Check out the final code example in the [Use fallback paywalls in Unity](unity-use-fallback-paywalls) page.
## Update paywall property access
The following properties have been moved from `AdaptyPaywall` to `AdaptyPlacement`:
```diff showLineNumbers
using AdaptySDK;
void ProcessPaywall(AdaptyPaywall paywall) {
- var abTestName = paywall.ABTestName;
- var audienceName = paywall.AudienceName;
- var revision = paywall.Revision;
- var placementId = paywall.PlacementId;
+ var abTestName = paywall.Placement.ABTestName;
+ var audienceName = paywall.Placement.AudienceName;
+ var revision = paywall.Placement.Revision;
+ var placementId = paywall.Placement.Id;
}
```
## Update remote config access
The remote config properties have been restructured into an `AdaptyRemoteConfig` object for better organization:
```diff showLineNumbers
using AdaptySDK;
void ProcessRemoteConfig(AdaptyPaywall paywall) {
- var remoteConfigString = paywall.RemoteConfigString;
- var locale = paywall.Locale;
- var remoteConfigDict = paywall.RemoteConfig;
+ var remoteConfigString = paywall.RemoteConfig.Data;
+ var locale = paywall.RemoteConfig.Locale;
+ var remoteConfigDict = paywall.RemoteConfig.Dictionary;
}
```
## Update AdaptyPaywall model usage
The `VendorProductIds` property has been deprecated in favor of `ProductIdentifiers`. The new property returns `AdaptyProductIdentifier` objects instead of simple strings, providing more structured product information.
```diff showLineNumbers
using AdaptySDK;
void ProcessPaywallProducts(AdaptyPaywall paywall) {
- var productIds = paywall.VendorProductIds;
- foreach (var vendorId in productIds) {
- // use vendorId
- }
+ var productIdentifiers = paywall.ProductIdentifiers;
+ foreach (var productId in productIdentifiers) {
+ var vendorId = productId.VendorProductId;
+ // use vendorId
+ }
}
```
The `AdaptyProductIdentifier` object provides access to the vendor product ID through the `VendorProductId` property, maintaining the same functionality while offering better structure for future enhancements.
## Update GetPaywall fetch policy
The `fetchPolicy` parameter type in the `GetPaywall` method has been changed from `AdaptyPaywallFetchPolicy` to `AdaptyPlacementFetchPolicy`. This change unifies the fetch policy usage across the SDK.
```diff showLineNumbers
using AdaptySDK;
void GetPaywall(string placementId) {
- Adapty.GetPaywall(placementId, AdaptyPaywallFetchPolicy.ReloadRevalidatingCacheData, null, (paywall, error) => {
+ Adapty.GetPaywall(placementId, AdaptyPlacementFetchPolicy.ReloadRevalidatingCacheData, null, (paywall, error) => {
// handle the result
});
}
```
---
# File: migration-to-unity-sdk-34
---
---
title: "Migrate Adapty Unity SDK to v. 3.4"
description: "Migrate to Adapty Unity SDK v3.4 for better performance and new monetization features."
---
Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end.
## Update fallback paywall files
Update your fallback paywall files to ensure compatibility with the new SDK version:
1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard.
2. [Replace the existing fallback paywalls in your mobile app](unity-use-fallback-paywalls) with the new files.
## Update implementation of Observer Mode
If you're using Observer Mode, make sure to update its implementation.
Previously, different methods were used to report transactions to Adapty. In the new version, the `reportTransaction` method should be used consistently across both Android and iOS. This method explicitly reports each transaction to Adapty, ensuring it's recognized. If a paywall was used, pass the variation ID to link the transaction to it.
:::warning
**Don't skip transaction reporting!**
If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations.
:::
```diff showLineNumbers
- #if UNITY_ANDROID && !UNITY_EDITOR
- Adapty.RestorePurchases((profile, error) => {
- // handle the error
- });
- #endif
Adapty.ReportTransaction(
"YOUR_TRANSACTION_ID",
"PAYWALL_VARIATION_ID", // optional
(error) => {
// handle the error
});
```
---
# File: migration-to-unity330
---
---
title: "Migrate Adapty Unity SDK to v. 3.3"
description: "Migrate to Adapty Unity SDK v3.3 for better performance and new monetization features."
---
Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you.
1. Upgrade to Adapty SDK v3.3.x.
2. Renamed multiple classes, properties, and methods in the Adapty and AdaptyUI modules of Adapty SDK.
3. From now on, the `SetLogLevel` method accepts a callback as an argument.
4. From now on, the `PresentCodeRedemptionSheet` method accepts a callback as an argument.
5. Change how the paywall view is created
6. Remove the `GetProductsIntroductoryOfferEligibility` method.
7. Save fallback paywalls to separate files (one per platform) in `Assets/StreamingAssets/` and pass the file names to the `SetFallbackPaywalls` method.
8. Update making purchase
9. Update handling of Paywall Builder events.
10. Update handling of Paywall Builder paywall errors.
11. Update integration configurations for Adjust, Amplitude, AppMetrica, Appsflyer, Branch, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh.
13. Update Observer mode implementation.
14. Update the Unity plugin initialization with an explicit `Activate` call.
## Upgrade Adapty Unity SDK to 3.3.x
Up to this version, Adapty SDK was the core and mandatory SDK necessary for the proper functioning of Adapty within your app, and AdaptyUI SDK was an optional SDK that becomes necessary only if you use the Adapty Paywall Builder.
Starting with version 3.3.0, AdaptyUI SDK is deprecated, and AdaptyUI is merged to Adapty SDK as a module. Because of these changes, you need to remove AdaptyUISDK and reinstall AdaptySDK.
1. Remove both **AdaptySDK** and **AdaptyUISDK** package dependencies from your project.
2. Delete the **AdaptySDK** and **AdaptyUISDK** folders.
3. Import the AdaptySDK package again as described in the [Adapty SDK installation & configuration for Unity](sdk-installation-unity) page.
## Renamings
1. Rename in Adapty module:
| Old version | New version |
| ------------------------- | ------------------------ |
| Adapty.sdkVersion | Adapty.SDKVersion |
| Adapty.LogLevel | AdaptyLogLevel |
| Adapty.Paywall | AdaptyPaywall |
| Adapty.PaywallFetchPolicy | AdaptyPaywallFetchPolicy |
| PaywallProduct | AdaptyPaywallProduct |
| Adapty.Profile | AdaptyProfile |
| Adapty.ProfileParameters | AdaptyProfileParameters |
| ProfileGender | AdaptyProfileGender |
| Error | AdaptyError |
2. Rename in AdaptyUI module:
| Old version | New version |
| ------------------ | ------------------ |
| CreatePaywallView | CreateView |
| PresentPaywallView | PresentView |
| DismissPaywallView | DismissView |
| AdaptyUI.View | AdaptyUIView |
| AdaptyUI.Action | AdaptyUIUserAction |
## Change the SetLogLevel method
From now on, the `SetLogLevel` method accepts a callback as an argument.
```diff showLineNumbers
- Adapty.SetLogLevel(Adapty.LogLevel.Verbose);
+ Adapty.SetLogLevel(Adapty.LogLevel.Verbose, null); // or you can pass the callback to handle the possible error
```
## Change the PresentCodeRedemptionSheet method
From now on, the `PresentCodeRedemptionSheet` method accepts a callback as an argument.
```diff showLineNumbers
- Adapty.PresentCodeRedemptionSheet();
+ Adapty.PresentCodeRedemptionSheet(null); // or you can pass the callback to handle the possible error
```
## Change how the paywall view is created
For the complete code example, check out [Fetch the view configuration of paywall designed using Paywall Builder](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder).
```diff showLineNumbers
+ var parameters = new AdaptyUICreateViewParameters()
+ .SetPreloadProducts(true);
- AdaptyUI.CreatePaywallView(
+ AdaptyUI.CreateView(
paywall,
- preloadProducts: true,
+ parameters,
(view, error) => {
// use the view
});
```
## Remove the GetProductsIntroductoryOfferEligibility method
Before Adapty iOS SDK 3.3.0, the product object always included offers, regardless of whether the user was eligible. You had to manually check eligibility before using the offer.
Now, the product object only includes an offer if the user is eligible. This means you no longer need to check eligibility — if an offer is present, the user is eligible.
## Update method for providing fallback paywalls
Up to this version, the fallback paywalls were passed as a serialized JSON. Starting from v 3.3.0, the mechanism is changed:
1. Save fallback paywalls to files in `/Assets/StreamingAssets/`, 1 file for Android and another for iOS.
2. Pass the file names to the `SetFallbackPaywalls` method.
Your code will change this way:
```diff showLineNumbers
using AdaptySDK;
void SetFallBackPaywalls() {
+ #if UNITY_IOS
+ var assetId = "adapty_fallback_ios.json";
+ #elif UNITY_ANDROID
+ var assetId = "adapty_fallback_android.json";
+ #else
+ var assetId = "";
+ #endif
- Adapty.SetFallbackPaywalls("FALLBACK_PAYWALLS_JSON_STRING", (error) => {
+ Adapty.SetFallbackPaywalls(assetId, (error) => {
// handle the error
});
}
```
Check out the final code example in the [Use fallback paywalls in Unity](unity-use-fallback-paywalls) page.
## Update making purchase
Previously canceled and pending purchases were considered errors and returned the `PaymentCancelled` and `PendingPurchase` codes, respectively.
Now a new `AdaptyPurchaseResultType` class is used to process canceled, successful, and pending purchases. Update the code of purchasing in the following way:
```diff showLineNumbers
using AdaptySDK;
void MakePurchase(AdaptyPaywallProduct product) {
- Adapty.MakePurchase(product, (profile, error) => {
- // handle successfull purchase
+ Adapty.MakePurchase(product, (result, error) => {
+ switch (result.Type) {
+ case AdaptyPurchaseResultType.Pending:
+ // handle pending purchase
+ break;
+ case AdaptyPurchaseResultType.UserCancelled:
+ // handle purchase cancellation
+ break;
+ case AdaptyPurchaseResultType.Success:
+ var profile = result.Profile;
+ // handle successful purchase
+ break;
+ default:
+ break;
}
});
}
```
Check out the final code example in the [Make purchases in mobile app](unity-making-purchases) page.
## Update handling of Paywall Builder events
Canceled and pending purchases are not considered to be errors any more, all these cases are processed with the `PaywallViewDidFinishPurchase` method.
1. Delete processing of the [Canceled purchase](unity-handling-events-legacy#canceled-purchase) event.
2. Update handling of the Successful purchase event in the following way:
```diff showLineNumbers
- public void OnFinishPurchase(
- AdaptyUI.View view,
- Adapty.PaywallProduct product,
- Adapty.Profile profile
- ) { }
+ public void PaywallViewDidFinishPurchase(
+ AdaptyUIView view,
+ AdaptyPaywallProduct product,
+ AdaptyPurchaseResult purchasedResult
+ ) { }
```
3. Update handling of actions:
```diff showLineNumbers
- public void OnPerformAction(
- AdaptyUI.View view,
- AdaptyUI.Action action
- ) {
+ public void PaywallViewDidPerformAction(
+ AdaptyUIView view,
+ AdaptyUIUserAction action
+ ) {
switch (action.Type) {
- case AdaptyUI.ActionType.Close:
+ case AdaptyUIUserActionType.Close:
view.Dismiss(null);
break;
- case AdaptyUI.ActionType.OpenUrl:
+ case AdaptyUIUserActionType.OpenUrl:
var urlString = action.Value;
if (urlString != null {
Application.OpenURL(urlString);
}
default:
// handle other events
break;
}
}
```
4. Update handling of started purchase:
```diff showLineNumbers
- public void OnSelectProduct(
- AdaptyUI.View view,
- Adapty.PaywallProduct product
- ) { }
+ public void PaywallViewDidSelectProduct(
+ AdaptyUIView view,
+ string productId
+ ) { }
```
5. Update handling of failed purchase:
```diff showLineNumbers
- public void OnFailPurchase(
- AdaptyUI.View view,
- Adapty.PaywallProduct product,
- Adapty.Error error
- ) { }
+ public void PaywallViewDidFailPurchase(
+ AdaptyUIView view,
+ AdaptyPaywallProduct product,
+ AdaptyError error
+ ) { }
```
6. Update handling of successful restore event:
```diff showLineNumbers
- public void OnFailRestore(
- AdaptyUI.View view,
- Adapty.Error error
- ) { }
+ public void PaywallViewDidFailRestore(
+ AdaptyUIView view,
+ AdaptyError error
+ ) { }
```
Check out the final code example in the [Handle paywall events](unity-handling-events) page.
## Update handling of Paywall Builder paywall errors
The handling of errors is changed as well, please update your code according to the guidance below.
1. Update the handling of the product loading errors:
```diff showLineNumbers
- public void OnFailLoadingProducts(
- AdaptyUI.View view,
- Adapty.Error error
- ) { }
+ public void PaywallViewDidFailLoadingProducts(
+ AdaptyUIView view,
+ AdaptyError error
+ ) { }
```
2. Update the handling of the rendering errors:
```diff showLineNumbers
- public void OnFailRendering(
- AdaptyUI.View view,
- Adapty.Error error
- ) { }
+ public void PaywallViewDidFailRendering(
+ AdaptyUIView view,
+ AdaptyError error
+ ) { }
```
## Update third-party integration SDK configuration
Starting with Adapty Unity SDK 3.3.0, we’ve updated the public API for the `updateAttribution` method. Previously, it accepted a `[AnyHashable: Any]` dictionary, allowing you to pass attribution objects directly from various services. Now, it requires a `[String: any Sendable]`, so you’ll need to convert attribution objects before passing them.
To ensure integrations work properly with Adapty Unity SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below.
### Adjust
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration).
```diff showLineNumbers
- using static AdaptySDK.Adapty;
using AdaptySDK;
Adjust.GetAdid((adid) => {
- Adjust.GetAttribution((attribution) => {
- Dictionary
2. Download the `adapty-ui-unity-plugin-*.unitypackage` from GitHub and import it into your project.
3. Download and import the [External Dependency Manager plugin](https://github.com/googlesamples/unity-jar-resolver).
4. The SDK uses the "External Dependency Manager" plugin to handle iOS Cocoapods dependencies and Android gradle dependencies. After the installation, you may need to invoke the dependency manager:
`Assets -> External Dependency Manager -> Android Resolver -> Force Resolve`
and
`Assets -> External Dependency Manager -> iOS Resolver -> Install Cocoapods`
5. When building your Unity project for iOS, you would get `Unity-iPhone.xcworkspace` file, which you have to open instead of `Unity-iPhone.xcodeproj`, otherwise, Cocoapods dependencies won't be used.
## Configure Adapty SDK
To configure the Adapty SDK for Unity, start by initializing the Adapty Unity Plugin and then using it as described in the guidance below. Additionally, ensure to set up your logging system to receive errors and other important information from Adapty.
### Activate Adapty SDK
You only need to activate the Adapty SDK once, typically early in your app's lifecycle.
```csharp showLineNumbers
using AdaptySDK;
var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY")
.SetCustomerUserId(null)
.SetObserverMode(false)
.SetServerCluster(AdaptyServerCluster.Default)
.SetIPAddressCollectionDisabled(false)
.SetIDFACollectionDisabled(false);
.SetActivateUI(true)
.SetAdaptyUIMediaCache(
100 * 1024 * 1024, // MemoryStorageTotalCostLimit 100MB
null, // MemoryStorageCountLimit
100 * 1024 * 1024 // DiskStorageSizeLimit 100MB
);
Adapty.Activate(builder.Build(), (error) => {
if (error != null) {
// handle the error
return;
}
});
```
| Parameter | Presence | Description |
| ------------------------------- | -------- | ------------------------------------------------------------ |
| apiKey | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [App settings-> General tab -> API keys subsection](https://app.adapty.io/settings/general) |
| logLevel | optional | Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels:A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
| | customerUserId | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. | | idfaCollectionDisabled | optional |Set to `true` to disable IDFA collection and sharing.
The default value is `false`.
For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
| | ipAddressCollectionDisabled | optional |Set to `true` to disable user IP address collection and sharing.
The default value is `false`.
| ### Use Adapty Unity Plugin 1. Create a script to listen to Adapty events. Name it `AdaptyListener` in your scene. We suggest using the `DontDestroyOnLoad` method for this object to ensure it persists throughout the application's lifespan.
Adapty uses `AdaptySDK` namespace. At the top of your script files that use the Adapty SDK, you may add
```csharp showLineNumbers title="C#"
using AdaptySDK;
```
2. Subscribe to Adapty events:
```csharp showLineNumbers title="C#"
using UnityEngine;
using AdaptySDK;
public class AdaptyListener : MonoBehaviour, AdaptyEventListener {
void Start() {
DontDestroyOnLoad(this.gameObject);
Adapty.SetEventListener(this);
}
public void OnLoadLatestProfile(Adapty.Profile profile) {
// handle updated profile data
}
}
```
Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to display the paywalls and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app.
---
# File: unity-get-legacy-pb-paywalls
---
---
title: "Fetch legacy Paywall Builder paywalls in Unity SDK"
description: "Retrieve legacy PB paywalls in your Unity app with Adapty SDK."
---
After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your Unity app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below.
:::warning
This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls.
- For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](unity-get-pb-paywalls).
- For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-unity).
:::
optional
default: `en`
|The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-unity). For Unity, the view configuration is automatically handled when you present the paywall using the `AdaptyUI.ShowPaywall()` method. --- # File: unity-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in Unity SDK" description: "" --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls**, which require Adapty SDK up to version 2.x. The [new Paywall Builder](adapty-paywall-builder) requires Adapty SDK 3.0 or later, which is currently not available for Unity. For presenting remote config paywalls, see [Render paywall designed by remote config](present-remote-config-paywalls). ::: To display a paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` --- # File: unity-handling-events-legacy --- --- title: "Handle paywall events in legacy Unity SDK" description: "" --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **legacy Paywall Builder paywalls**, which require Adapty SDK up to version 2.x. The [new Paywall Builder](adapty-paywall-builder) requires Adapty SDK 3.0 or later, which is currently not available for Unity. ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIEventListener` methods and register the observer before presenting any screen: ```csharp showLineNumbers title="Unity" AdaptyUI.SetEventListener(this); ``` ### User-generated events #### Actions If a user has performed some action, this method will be invoked: ```csharp showLineNumbers title="Unity" public void OnPerformAction(AdaptyUI.View view, AdaptyUI.Action action) { switch (action.Type) { case AdaptyUI.ActionType.Close: view.Dismiss(null); break; case AdaptyUI.ActionType.OpenUrl: var urlString = action.Value; if (urlString != null { Application.OpenURL(urlString); } default: // handle other events break; } } ``` The following action types are supported: - `Close` - `OpenUrl` - `Custom` - `AndroidSystemBack`. At the very least you need to implement the reactions to both `close` and `openURL`. For example, if a user taps the close button, the action `Close` will occur and you are supposed to dismiss the paywall. Note that `AdaptyUI.Action` has optional value property: look at this in the case of `OpenUrl` and `Custom`. > 💡 Login Action > > If you have configured Login Action in the dashboard, you should implement reaction for `Custom` action with value `"login"` #### Product selection If a product was selected for purchase (by a user or by the system), this method will be invoked. ```csharp showLineNumbers title="Unity" public void OnSelectProduct(AdaptyUI.View view, Adapty.PaywallProduct product) { } ``` #### Started purchase If a user initiates the purchase process, this method will be invoked. ```csharp showLineNumbers title="Unity" public void OnStartPurchase(AdaptyUI.View view, Adapty.PaywallProduct product) { } ``` #### Canceled purchase If a user initiates the purchase process but manually interrupts it, this method will be invoked. This event occurs when the `Adapty.MakePurchase()` function completes with a `.paymentCancelled` error: ```csharp showLineNumbers title="Unity" public void OnCancelPurchase(AdaptyUI.View view, Adapty.PaywallProduct product) { } ``` #### Successful purchase If `Adapty.MakePurchase()` succeeds, this method will be invoked: ```csharp showLineNumbers title="Unity" public void OnFinishPurchase(AdaptyUI.View view, Adapty.PaywallProduct product, Adapty.Profile profile) { } ``` We recommend dismissing the screen in that case. #### Failed purchase If `Adapty.MakePurchase()` fails, this method will be invoked: ```csharp showLineNumbers title="Unity" public void OnFailPurchase(AdaptyUI.View view, Adapty.PaywallProduct product, Adapty.Error error) { } ``` #### Successful restore If `Adapty.RestorePurchases()` succeeds, this method will be invoked: ```csharp showLineNumbers title="Unity" public void OnFinishRestore(AdaptyUI.View view, Adapty.Profile profile) { } ``` We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](unity-listen-subscription-changes.md) topic to learn how to check it. #### Failed restore If `Adapty.RestorePurchases()` fails, this method will be invoked: ```csharp showLineNumbers title="Unity" public void OnFailRestore(AdaptyUI.View view, Adapty.Error error) { } ``` ### Data fetching and rendering #### Product loading errors If you didn't pass the product array during initialization, AdaptyUI will retrieve the necessary objects from the server by itself. In this case, this operation may fail, and AdaptyUI will report the error by invoking this method: ```csharp showLineNumbers title="Unity" public void OnFailLoadingProducts(AdaptyUI.View view, Adapty.Error error) { } ``` #### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method: ```csharp showLineNumbers title="Unity" public void OnFailRendering(AdaptyUI.View view, Adapty.Error error) { } ``` In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: unity-hide-legacy-paywall-builder-paywalls --- --- title: "Hide legacy Paywall Builder paywalls in Unity SDK" description: "Hide legacy paywalls in your Unity app with Adapty SDK." --- While Paywall Builder seamlessly handles the purchasing process upon clicking "buy" buttons, you have to manage the closure of paywall screens within your Unity app. :::warning This guide covers only hiding **legacy Paywall Builder paywalls** which supports Adapty SDK v2.x or earlier. ::: You can hide a paywall view by calling the `view.Dismiss` method. ```csharp showLineNumbers view.Dismiss((error) => { // handle the error }); ``` --- # End of Documentation _Generated on: 2026-03-05T16:27:48.719Z_ _Successfully processed: 42/42 files_