How to use RevenueCat server-to-server events for SKAdNetwork attribution
A practical guide to tracking trial conversions and improving SKAdNetwork attribution with RevenueCat.

With the advancement of Apple’s privacy-centric API, SKAdNetwork (and the addition of the newest framework, Ad Attribution Kit, in parallel), subscription apps must adapt to the latest changes and profit from the latest features.
The guide will cover how to properly implement server-to-server events in the code base, comply with Apple’s rules, and measure the impact of your marketing efforts.
The context
Marketing goals
According to RevenueCat’s 2024 State of Subscription Apps report, nearly 70% of subscription apps offer a free trial as part of their pricing strategy. This means many companies tend to care about the conversion rate from trial to paid or the number of users who pay for the initial package after the trial period ends.
Apps must significantly challenge themselves to accurately measure the number of paying users they acquire through user acquisition activities. The new and extended attribution windows, using SKAdNetwork 4.0 as the primary source of truth and following its documentation, are the best way to understand the profitability of ad campaigns in a deterministic way.
This delayed conversion makes tracking the effectiveness of user acquisition more challenging. You want to measure the conversion of ads into trials and how different campaigns differ in the trial to paying customer conversion.
Mobile marketers will expect different scenarios depending on the duration of the free trial. For example:
- For apps with a trial lasting less than seven days, window two (postback sequence = 1) will be the primary moment to trigger the event trial_converted.
- For apps with a seven-day trial period, window three (postback sequence = 2) is the right window to analyze the conversion rate from free to paid.
Many apps optimize their marketing campaigns using the “Start Trial” event. This event is paramount because it accurately measures all in-app events in the application flow, enlightening developers on where to focus their efforts.
SKAdNetwork limitations
To illustrate the case, imagine the following scenario:
- An iOS subscription has a free trial of 3 days.
- The app is actively spending money on ads through Meta Ads.
- The app uses the Meta SDK and RevenueCat.
- The Meta campaigns are optimizing towards “Start Trial”.
- The ‘Trial Converted’ event (S2S) holds the highest priority value (fine and coarse) across the conversion values mapping in all the SKAdNetwork windows. This is because it’s crucial for measuring revenue and the conversion rate from trial to paid. Understanding and implementing this event is key to your app’s success.
Most marketers will think that adding the “Trial Converted” (S2S event) into the conversion value mapping in Meta as Subscribe will instantly update the conversion values on the codebase.
That’s far from the truth because SKAdNetwork only allows an app to update its conversion values when the user interacts with it. In other words, to measure a specific in-app conversion defined by the developer, the user must at least open the app. The developer needs to implement a particular logic on the codebase to trigger the conversion when the app open event happens (more on this later)
This is a big deal because apps can’t easily measure:
- The quality of the users they end up paying for
- The total estimated revenue generated by an ad network or campaign
- Critical events that occur when the app is closed, like the “Trial Converted”
RevenueCat events
To understand how you can navigate this challenge and find the best solution for your specific case, this guide will show several alternative implementations, with and without RevenueCat events, and how using RevenueCat events might help you streamline and improve your implementation. The code snippets below will be using the RevenueCat event signal associated with the “Trial Converted”, that gets triggered when a user switches from a free trial to a paid subscription.
The reason is simple: The application user on the front end does not trigger this event; instead, it is triggered in the back end without the user taking any action.
Overcoming the challenge
What to expect
Before exploring the solutions you can implement to tackle this challenge and start measuring any server-to-server event through SKAdNetwrok, you must understand that each alternative has pros and cons and that the examples are just examples. You may need to make some changes or customizations to incorporate them into your codebase.
As mentioned in this RevenueCat forum thread: https://community.revenuecat.com/sdks-51/skadnetwork-listen-to-trial-conversion-events-app-side-2886/. This topic has been discussed for some time, and the goal of these solutions is to give you some fresh ideas on how to mitigate the problem.
Saying that, let’s jump into the first solution.
Client-side verification
The proposed solution is to implement a manager that records the start of the trial period, the start and expiration dates and validates the user each time he/she opens the app. If we detect that the user has moved from a trial period to a premium, we send the corresponding event to SKAdNetwork.
One key step in implementing this solution is recording the necessary data about the trial period when the user makes a purchase. This includes the start date, the length of the trial period, and its expiration date.
Purchase Function
In this example, we show how to record this data when making a purchase using RevenueCat:
1private func purchase(package: Package) {
2 Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
3 if let error = error {
4 print("Error when making the purchase: \(error.localizedDescription)")
5 return
6 }
7
8 guard let entitlement = customerInfo?.entitlements["Pro"],
9 entitlement.isActive else {
10 print("Subscription could not be activated.")
11 return
12 }
13
14 // Manage trial period
15 if entitlement.periodType == .trial,
16 let introPrice = package.storeProduct.introductoryDiscount,
17 introPrice.price == 0 {
18 let trialPeriod = introPrice.subscriptionPeriod
19 let trialStartDate = Date()
20
21 // Convert period to the correct components according to unit
22 let trialDuration: DateComponents
23 switch trialPeriod.unit {
24 case .day:
25 trialDuration = DateComponents(day: trialPeriod.value)
26 case .week:
27 trialDuration = DateComponents(day: trialPeriod.value * 7)
28 case .month:
29 trialDuration = DateComponents(month: trialPeriod.value)
30 case .year:
31 trialDuration = DateComponents(year: trialPeriod.value)
32 @unknown default:
33 trialDuration = DateComponents(day: trialPeriod.value)
34 }
35
36 let trialExpirationDate = Calendar.current.date(
37 byAdding: trialDuration,
38 to: trialStartDate
39 )
40
41 // Save trial status and dates
42 let userDefaults = UserDefaults.standard
43 userDefaults.set(true, forKey: "isTrialStarted")
44 userDefaults.set(trialStartDate, forKey: "trialStartDate")
45 userDefaults.set(trialExpirationDate, forKey: "trialExpirationDate")
46 }
47 }
48}
49
This code is executed at the place where the purchase is made (e.g., from a button on a paywall). Here, we dynamically record the start and duration of the trial period directly from RevenueCat data.
The TrialConversionManager
To complement the trial period management, we implemented a manager that performs checks each time the app enters the foreground. The goal is to detect whether the user has switched from trial to premium and, if so, send the event to SKAdNetwork.
1final class TrialConversionManager {
2
3 static let shared = TrialConversionManager()
4
5 private let userDefaults = UserDefaults.standard
6
7 func checkTrialStatus() {
8
9 guard userDefaults.bool(forKey: "isTrialStarted"),
10
11 let trialExpirationDate = userDefaults.object(forKey: "trialExpirationDate") as? Date,
12
13 Date() >= trialExpirationDate else {
14
15 return
16
17 }
18
19 Purchases.shared.getCustomerInfo { customerInfo, _ in
20
21 if let entitlement = customerInfo?.entitlements["Pro"],
22
23 entitlement.isActive && entitlement.periodType == .normal {
24
25 // Send event to SKAdNetwork
26
27 SKAdNetworkManager.shared.sendConversionValue(.premiumTrialConverted)
28
29 // Reset flag
30
31 self.userDefaults.set(false, forKey: "isTrialStarted")
32
33 }
34
35 }
36
37 }
38
39}
Pros:
- It is simple and effective.
- It allows you to manage conversion values in an autonomous and generalized way.
- Does not require additional server infrastructure.
Cons:
- It depends on the user opening the app before the SKAdNetwork window ends.
- If the user manipulates the device time, calculations could be affected, although validations on the RevenueCat server mitigate this risk.
In the next section, we will explore how to complement this approach with silent push notifications to improve the reliability of sending conversion values in real-time.
Silent push
In this section, we explore how silent push notifications can improve the reliability of Conversion Values (CV) management in SKAdNetwork. This strategy requires a server that logs key events and acts as an intermediary to ensure the app receives timely conversion events.
The problem is that depending solely on the user’s interaction with the app, certain critical events may not be recorded correctly, especially if the user does not open the app during the time windows defined by SKAdNetwork. To mitigate this, we integrate a server that:
- Logs data from trial periods and their premium conversion.
- Communicates to the app when to send events to SKAdNetwork, using silent push notifications.
Solution 2.1: Complementing the local solution with a server
In this solution, the server extends the local logic, helping to ensure that the app processes key events on time. The server does not communicate directly with Apple’s systems but calculates the expiration of the test period and triggers the local logic in the app to perform the necessary checks.
Workflow:
- Trial period start
- The app sends the server information about the start of the trial, including:
- Push token (pushToken).
- Trial start date.
- Trial duration.
- This information is registered on the server.
- The app sends the server information about the start of the trial, including:
- Trial expiration calculation
- The server calculates when the trial period will expire based on the data received.
- When the trial has expired, a silent push notification is sent to the app
- Validation in the application
- Upon receiving the notification, the app raises the TrialConversionManager and checks if the user has converted to premium.
- If the user is premium, it sends the corresponding event to SKAdNetwork.
- If the user is still on trial, we could communicate it to the server to try again later until the TrialConversionManager tells us that the user is no longer on trial, where the server would stop checking for that user.
Advantages:
- Increases the probability that the conversion will be registered within the SKAdNetwork windows.
- It is an intermediate solution, leveraging existing local logic.
Disadvantages:
- Does not entirely eliminate the need for in-app checks.
Solution 2.2: Full Integration with Apple’s Server
In this solution, the server acts as a centralized system, logging both trial startup events and subscription status changes (e.g., when a user upgrades to premium, which is the one we are interested in). The server receives this information directly from Apple’s systems via Server-to-Server Notifications.
Workflow:
Trial period registration:
- The app sends the server information about the start of the trial, including:
- Push token (pushToken).
- originalTransactionId (transaction identifier to identify the user when Apple communicates that he/she is upgrading to premium).
- Trial start date.
- Trial duration.
(The last two in case you want to keep a record on the server, but it is unnecessary for the operation when receiving the events from Apple).
- Receiving Apple notifications:
- When Apple detects that a user has converted to premium (i.e., the subscription has officially started), it sends a notification to the server.
- This notification includes information about the updated status of the subscription.
- Silent push notification:
- The server sends a silent push notification to the corresponding device, informing that the user is premium.
- Upon receiving this notification, the app registers the event in SKAdNetwork without needing additional checks.
Advantages:
- It is more accurate, as the server receives conversion events directly from Apple.
- Reduces the logic needed in the app, eliminating the dependency on the TrialConversionManager.
Disadvantages:
- It requires more advanced integration with Apple’s server-to-server notifications.
- It entirely relies on a server for event management.
Key takeaways
- Use RevenueCat’s S2S events to track trial conversions even when users aren’t in the app (remember that this does not guarantee a 100% rate accuracy). RevenueCat can act as the backend service that receives Apple’s server notifications and processes trial conversions.
- Combine client-side tracking with silent push notifications to improve SKAdNetwork accuracy. If your app is using RevenueCat, you can use its S2S events to trigger silent push notifications at the right time.
- For the most reliable attribution, integrate Apple’s server-to-server notifications and use RevenueCat to process those events. This ensures conversions are tracked without needing the user to open the app.
- Map conversion values strategically in ad platforms like Meta for better insights.
- Continuously test and refine your mapping setup as SKAN/AAK evolves.
A deep knowledge of the primary measurement-centric-API works is essential to leverage your marketing efforts, specifically for subscription-based apps. Although many of the readers will be marketers with a non-technical background, learning and sharing content that helps close the gap between development and growth is necessary.
Mobile apps could build efficient solutions to mitigate the effects of SKAdNetwork/AdAttributionKit by implementing some of these techniques or thinking creatively, using this guide as a starting point.
Never forget that all tips and ideas rely on the layer of Apple policies and comply with its conditions.
Special mention to Antonio Jiménez Martínez for contributing all the technical knowledge to the article.
You might also like
- Blog post
Is it time to stop offering free trials? Here’s why you should A/B test it
Free trials can boost conversions, but they can also drain revenue. Should your app take the risk?
- Blog post
Why Google App Campaigns is high reward, high maintenance — Ashley Black, Candid Consulting
Google’s ad platform offers huge growth potential... but only if you keep a close eye on where your budget is going
- Blog post
Five North Star Metrics that drive real subscription growth
The key metrics that lead to sustainable subscription growth.