diff --git a/.github/styles/Microsoft/HeadingAcronyms.yml b/.github/styles/Microsoft/HeadingAcronyms.yml index 6674d3e80..cebf603f7 100644 --- a/.github/styles/Microsoft/HeadingAcronyms.yml +++ b/.github/styles/Microsoft/HeadingAcronyms.yml @@ -24,3 +24,5 @@ exceptions: - AAR - CPU - IDE + - IDFA + - IDFV diff --git a/src/content/docs/api/s2s-api/attribution-checklist.mdx b/src/content/docs/api/s2s-api/attribution-checklist.mdx deleted file mode 100644 index 04fe4f611..000000000 --- a/src/content/docs/api/s2s-api/attribution-checklist.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: "Server-to-server (S2S) attribution checklist" -description: "Adjust's S2S attribution checklist provides a rundown of the requirements for your in-app solution." -slug: en/api/s2s-api/attribution-checklist -sidebar-position: 5 -sidebar-label: S2S attribution checklist ---- - -Server-to-server (S2S) attribution and session measurement requires a custom in-app solution that replicates the Adjust SDK’s basic functionality. This S2S attribution checklist provides a rundown of the requirements for your in-app solution. Meeting these requirements guarantees the security of the information Adjust receives and the accuracy of reports. - -## [Before you begin](before-you-begin) - -Explore the Adjust SDK's minimal extent of code required for attribution and session measurement. Integrating this code into your app is Adjust’s preferred method of attribution and session measurement. The code is easy to integrate and provides the functionality that your custom solution will have to replicate. - -Follow the guides linked below to see the basic integration steps. - -📖 [Android](/en/sdk/android) / [iOS](/en/sdk/ios) / [Windows](/en/sdk/windows) / [Adobe Air](https://github.com/adjust/adobe_air_sdk#basic-integration) / [Unity](/en/sdk/unity) / [Cordova](https://github.com/adjust/cordova_sdk#basic-integration) / [Marmalade](https://github.com/adjust/marmalade_sdk#basic-integration) / [Xamarin](https://github.com/adjust/xamarin_sdk#basic-integration) / [Cocos2d-x](https://github.com/adjust/cocos2dx_sdk#basic-integration) / [React Native](/en/sdk/react-native) / [Titanium](https://github.com/adjust/titanium_sdk#basic-integration) / [Corona](https://github.com/adjust/corona_sdk#basic-integration) - -## [Checklist overview](checklist-overview) - -Adjust’s server-to-server attribution checklist covers 5 requirements under 3 key areas (data integrity and security, advertising and device IDs, and third-party integrations). Every item is fundamental to accurate attribution and session measurement. Your in-app solution will have to reproduce the Adjust SDK’s basic functionality, which meets these requirements by default. - -## [1. Maintain security and integrity](1-maintain-security-and-integrity) - -You must guarantee the security and integrity of the information your app creates, collects, and sends to your server by securing your requests and buffering information locally. - -### [Secure app-to-server requests](secure-app-to-server-requests) - -Mobile app install fraud is prevalent within the mobile industry and has cost marketers billions of dollars. The first essential step to defending against mobile app install fraud is to secure your app-to-server requests. If you can't guarantee your data’s security, Adjust can't know whether the information it receives from your server is legitimate or not. This leaves you vulnerable to fraudulent data within your reporting. - -If you don't meet this requirement, you are susceptible to spoofed installs in your reporting and expenditure, which can negatively impact your ad budget. If you can't guarantee the security of your requests, an s2s integration is discouraged. - -#### [How Adjust secures installs](how-adjust-secures-installs) - -Adjust secures every install through the [Adjust SDK Signature](https://help.adjust.com/en/article/sdk-signature). This signature is a cryptographic hash secured with an [App Secret](https://help.adjust.com/en/article/sdk-signature#what-is-an-app-secret), which is implemented into the Adjust SDK and sent with every reported install. Adjust verifies this hash on every install and denies attribution to any traffic that can't be verified. - -### [Buffer information locally](buffer-information-locally) - -Users might open your app for the first time (counted as an **install**) or trigger sessions while their device is offline. Accurate attribution is impossible if offline activity never reaches Adjust's servers. - -If you don’t meet this requirement any short outages such as 4G handovers, or longer periods without network or WiFi coverage will result in data loss. In total, 10–20% of installs don't reach Adjust upon first attempt. If Adjust doesn't receive this data, attribution is performed based on the current data, rather than what actually occurred. - -#### [How Adjust buffers information](how-adjust-buffers-information) - -The Adjust SDK places all in-app activity in a queue, so it can send the data to Adjust's servers when a connection is available. - -## [2. Collect and create advertising and device IDs](2-collect-and-create-advertising-and-device-ids) - -Android’s Google Play Store advertising ID (GPS_ADID) and iOS’s ID for advertisers (IDFA) are both [advertising IDs](https://help.adjust.com/en/article/device-identifiers#advertising-ids). The device user can reset - or disable access to - both of these IDs. Therefore, Adjust also relies on device IDs and Universally unique identifiers (UUIDs) (iOS only) for attribution and session measurement. Both of these IDs can't be reset by the end user without resetting their device. - -### [Gather every possible advertising and device ID](gather-every-possible-advertising-and-device-id) - -Advertising IDs are resettable. Deliberate, repeated resetting of advertising IDs is common (for example: to cheat in-app reward systems). Also, around 15% of users on iOS have Limit Ad Tracking (LAT) enabled, thereby disabling access to their IDFA. For these reasons, Adjust relies on additional IDs to accurately attribute and continuously record in-app sessions. - -If you don’t meet this requirement every session recorded without a previously recorded advertising ID or without an advertising ID (all LAT-enabled users on iOS) will be attributed as a new install. - -#### [How Adjust collects IDs](how-adjust-collects-ids) - -The Adjust SDK collects every legally available advertising and device ID by default. Adjust maps these IDs, so, if one is reset, the new ID can be mapped to other IDs already held the system for that user. - -### [Generate a universally unique identifier and persist it to the device keychain (iOS)](generate-a-universally-unique-identifier-and-persist-it-to-the-device-keychain-ios) - -When users reset their advertising ID, uninstall and reinstall your app, or enable LAT, Adjust won't be able to retrieve their IDFA and/or IDFV. To continuously record users’ in-app sessions, Adjust relies on a permanent, locally generated UUID persisted to the device keychain. Adjust maps the UUID to other device information. This allows Adjust to seamlessly measure the user’s in-app activity when: - -- A user enables LAT -- A user resets their advertising ID -- Adjust doesn't receive the original advertising ID and/or ID for vendors (IDFV) on iOS - -If you don’t meet this requirement any reporting is likely to include installs originating from device farms, where advertising IDs are continuously reset to simulate fresh installs. Any user who enables LAT will be attributed as a new install upon each session. - -#### [How Adjust manages UUIDs](how-adjust-manages-uuids) - -Adjust generates a UUID upon install. This is mapped to other device information in Adjust's systems. - -## [3. Third-party integrations and additional data](3-third-party-integrations-and-additional-data) - -Critical information required for attribution to Apple Search Ads, the Google Play Store and third-party app stores (for example: Amazon Appstore) can only be collected within your app through third-party integrations. - -You must support the following: - -### [Android](android) - -- [Google Play Store referrer API](https://developer.android.com/google/play/installreferrer/igetinstallreferrerservice.html). -- Collection of the [instruction set for Dalvik VM detected through inflection](https://android.googlesource.com/platform/libcore/+/master/libart/src/main/java/dalvik/system/VMRuntime.java#109). -- [Deep link reattribution](https://developer.android.com/training/app-links/deep-linking.html). - -### [iOS](ios) - -- [Apple Search Ads attribution API](https://searchads.apple.com/help/reporting/0028-apple-ads-attribution-api). -- [Deep link reattribution](https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html). - -This information, collected within your app, must also be forwarded to Adjust immediately upon receipt to be considered for attribution. - -### [Collect the necessary data for attribution across all sources](collect-the-necessary-data-for-attribution-across-all-sources) - -Adjust relies on the information sent through these integrations for accurate and comprehensive attribution and deeplink reattribution. - -Without the Google Play Store referrer, Adjust will be unable to attribute: - -- Over 50% of Android installs -- All Google organic search installs -- Any third-party app store installs - -Additionally, [click injection filtering](https://help.adjust.com/en/article/click-injection-filtering) won't work. This will leave you vulnerable to a major source of mobile ad fraud on Android. Furthermore, without the instruction set for Dalvik VM, Adjust won't know if installs originate from virtual devices. - -Without the Apple Search Ads attribution API, Adjust will be unable to attribute: - -- Apple Search Ads installs - -#### [How Adjust collects the necessary attribution data](how-adjust-collects-the-necessary-attribution-data) - -The code required for these integrations is available in the Adjust SDK guides for [Android](/en/sdk/android) and [iOS](/en/sdk/ios). You can also enable deep link reattribution with the Adjust SDK by following the steps in the deep linking guides for [Android](/en/sdk/android/features/deep-links) and [iOS](/en/sdk/ios/features/deep-links). - -Collection of the instruction set for Dalvik VM occurs automatically when the Adjust Android SDK is added to your app. diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx new file mode 100644 index 000000000..f9c6d8301 --- /dev/null +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -0,0 +1,1692 @@ +--- +title: "S2S developer guide" +description: "Implement Adjust 100% S2S" +slug: en/api/s2s-api/s2s-developer-guide +sidebar-position: 6 +--- + +Adjust offers a server-to-server (S2S) interface for mobile, console, and Connected TV (CTV) apps. If you choose to implement Adjust through S2S, you need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions for the following: + +- How to make the necessary updates to your app. +- How to send S2S requests to Adjust. + + + +If your app needs to comply with COPPA, use the Adjust SDK and leverage its built-in COPPA features. Don't send non-compliant data using the S2S interface. + + + +## [Before you begin](before-you-begin) + +Here's what you need to do before you get started. + +### [Enable S2S session measurement](enable-s2s-session-measurement) + +If you're using the S2S interface for a mobile app, Adjust needs to enable S2S session measurement for your app. Contact your Adjust representative or support@adjust.com to proceed. + +S2S session measurement is enabled automatically for [CTV measurement](https://help.adjust.com/en/article/ctv-advision) and [PC and Console measurement](https://help.adjust.com/en/article/measurement-for-pc-and-console-games) when you select a platform. + +### [Set up S2S Security](set-up-s2s-security) + +[Implement S2S Security](/en/api/s2s-api/security) to safeguard your S2S activities and prevent spoofed requests. Generate a token in your Adjust dashboard and include it in each incoming request. + + + +Adjust's servers reject any requests without the correct token. This ensures robust protection for your data. + + + +### [Queue and persist events locally](queue-and-persist-events-locally) + +Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to Adjust's servers. + +To implement a local event queue with persistence: + +1. Create a queue to store activities when they occur. +2. For each activity, include a `created_at_unix` timestamp in seconds (for example: `1484085154`) representing when the event occurred on the device. +3. Save this queue to local storage (for example: SQLite database or files) to persist across app restarts. +4. Attempt to send activities to Adjust's servers when the queue is non-empty and the device is online. +5. Remove activities from the queue only after successful transmission. + +This approach helps mitigate data loss in the following scenarios: + +- Brief network interruptions (for example: 5G to WiFi handovers). +- Extended periods without connectivity. +- App crashes or force closes before transmission. + +Without local queuing, you risk losing 10–20% of install data, which can significantly impact attribution accuracy. By implementing this queuing system, you ensure that Adjust receives a complete and accurate picture of user activity, enabling precise attribution even for events that occur offline. + +### [Add iOS frameworks](add-ios-frameworks) + +You must link frameworks to your project to support certain iOS features. To add frameworks to your project: + +1. Open your project in Xcode. +2. Select your target in the project navigator. +3. Go to the **General** tab. +4. Scroll to the **Frameworks, Libraries, and Embedded Content** section. +5. Select the **\+** button. +6. Search for and add the frameworks that your app requires from the list below. + + + +| Framework | Description | +| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `AdSupport.framework` | Required to collect IDFA. Also required to collect Limit Ad Tracking status for pre-ATT iOS versions. | +| `AppTrackingTransparency.framework` | Required to show the AppTrackingTransparency prompt and collect IDFA on devices running iOS 14.5 and later. | +| `AdServices.framework` | Required for Adjust to perform attribution for Apple Search Ads campaigns. | +| `StoreKit.framework` | Required to run SKAdNetwork campaigns. | + +
+ +## [Required parameters](required-parameters) + +The following parameters are required in each S2S request. + + + +| Parameter | Description | +| ----------- | ----------------------------------------------------------------------- | +| `s2s` | Indicates that the request is an S2S request. Must be hardcoded to `1`. | +| `app_token` | Your Adjust app token. | +| `os_name` | The name of the mobile operating system. See the list of options below. | + +
+ + + +- `android` +- `android-tv` +- `apple-tv` +- `bada` +- `blackberry` +- `fire-tv` +- `ios` +- `linux` +- `macos` +- `nintendo` +- `playstation` + +--- + +- `roku-os` +- `server` +- `smart-cast` +- `steamos` +- `symbian` +- `tizen` +- `unknown` +- `webos` +- `windows` +- `windows-phone` +- `xbox` + + + +Create these parameters on your server. They will be used for all S2S requests to Adjust, regardless of the device. Additional device-specific parameters will be added as needed. For simplicity, this guide demonstrates all parameter handling on the client side, though in practice, much of this will occur server-side. + + + + +```swift +// Create dictionary for params to include on all S2S requests to Adjust +var params: [String: String] = [:] + +// Hard-coded +params["s2s"] = "1" + +// The name of the operating system running on the device +params["os_name"] = "ios" + +// Replace with your Adjust app token +params["app_token"] = "4w565xzmb54d" +``` + + + + +```objc +// Create dictionary for params to include on all S2S requests to Adjust +NSMutableDictionary *params = [NSMutableDictionary dictionary]; + +// Hard-coded +params[@"s2s"] = @"1"; + +// The name of the operating system running on the device +params[@"os_name"] = @"ios"; + +// Replace with your Adjust app token +params[@"app_token"] = @"4w565xzmb54d"; +``` + + + + +```kotlin +// Create map for params to include on all S2S requests to Adjust +val params = mutableMapOf() + +// Hard-coded +params["s2s"] = "1" + +// The name of the operating system running on the device +params["os_name"] = "android" + +// Replace with your Adjust app token +params["app_token"] = "4w565xzmb54d" +``` + + + + +```java +// Create map for params to include on all S2S requests to Adjust +Map params = new HashMap<>(); + +// Hard-coded +params.put("s2s", "1"); + +// The name of the operating system running on the device +params.put("os_name", "android"); + +// Replace with your Adjust app token +params.put("app_token", "4w565xzmb54d"); +``` + + + + +### [Device IDs and tracking statuses](device-ids-and-tracking-statuses) + +Every S2S request must include **at least one** device identifier. Due to privacy measures implemented by mobile operating systems, the advertising ID may not always be available. Therefore, you should include the advertising ID when available and must always include backup identifiers. + +Adjust also requires tracking statuses in each request. + +#### [PC and Console/CTV device IDs](pc-console-ctv-device-ids) + +For PC and Console and CTV measurement, you can pass a unique `external_device_id` parameter with each call to use as a device identifier. This value can be any unique string that identifies the device. + +#### [iOS device IDs](ios-device-ids) + +##### IDFA + +The ID for Advertisers (IDFA) is available only for iOS devices where users have opted to share it with your app via the App Tracking Transparency (ATT) prompt. Follow these steps if you choose to show the ATT prompt and collect IDFA in your app: + +1. Add ATT description in Xcode. + 1. Open your project's `Info.plist` file. + 2. In the editor, right-click on **Information Property List** and choose **Add Row** to add a key to the root. + 3. Set the key to `NSUserTrackingUsageDescription`. + 4. Set the value to a string explaining why you're requesting tracking permission (for example: "This identifier will be used to deliver personalized ads to you."). Be sure to review [Apple's guidelines](https://developer.apple.com/documentation/bundleresources/information_property_list/nsusertrackingusagedescription) for this text. +2. Implement ATT prompt and IDFA retrieval. + +ATT has the following requirements: + +- While ATT support begins with iOS 14, user consent for IDFA retrieval is only required from iOS 14.5 onwards. Therefore, Adjust recommends targeting the ATT prompt specifically to users on iOS 14.5 and later versions. +- The ATT prompt requires an active app state to display. Showing it immediately after other system prompts may fail unless you first confirm the app state is again active. +- The earliest places to show the prompt are in `applicationDidBecomeActive` (App Delegate) or `sceneDidBecomeActive` (Scene Delegate). It's not possible to show the ATT prompt in `didFinishLaunchingWithOptions` (App Delegate). + +The following code example addresses all of these requirements: + + + + +```swift +import AppTrackingTransparency +import AdSupport +import UIKit + +struct IDFAInfo { + let idfa: UUID? + let attStatus: ATTrackingManager.AuthorizationStatus? + let trackingEnabled: Bool? +} + +func getIDFAInfo(completion: @escaping (IDFAInfo) -> Void) { + // Show ATT prompt to get IDFA and updated ATT status + if #available(iOS 14.5, *) { + ATTrackingManager.requestTrackingAuthorization { status in + DispatchQueue.main.async { + let idfa = (status == .authorized) ? + ASIdentifierManager.shared().advertisingIdentifier : nil + completion(IDFAInfo(idfa: idfa, attStatus: status, trackingEnabled: + nil)) + } + } + // Don't show ATT prompt. Just get IDFA and tracking status. + } else { + let manager = ASIdentifierManager.shared() + let trackingEnabled = manager.isAdvertisingTrackingEnabled + let idfa = trackingEnabled ? manager.advertisingIdentifier : nil + completion(IDFAInfo(idfa: idfa, attStatus: nil, trackingEnabled: + trackingEnabled)) + } +} + +private func requestTrackingAuthorization( + completion: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) { + let trackingStatus = ATTrackingManager.trackingAuthorizationStatus + switch trackingStatus { + + // Only show ATT prompt if status isn't determined + case .notDetermined: + // If app state is active, show ATT prompt + if UIApplication.shared.applicationState == .active { + ATTrackingManager.requestTrackingAuthorization { status in + completion(status) + } + // Wait until app state is active, then show ATT prompt + } else { + NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + ATTrackingManager.requestTrackingAuthorization { status in + completion(status) + } + } + } + + // For all other statuses, return existing status + default: + completion(trackingStatus) + } +} + +// Usage example +getIDFAInfo { info in + // Include IDFA if available + if let idfa = info.idfa?.uuidString { + params["idfa"] = idfa + } + + // Include either ATT status or tracking status, never both + if let attStatus = info.attStatus { + params["att_status"] = String(attStatus.rawValue) + } else if let trackingEnabled = info.trackingEnabled { + params["tracking_enabled"] = trackingEnabled ? "1" : "0" + } +} +``` + + + + +```objc +#import +#import +#import + +@interface IDFAInfo : NSObject +@property (nonatomic, strong) NSUUID *idfa; +@property (nonatomic, assign) ATTrackingManagerAuthorizationStatus attStatus; +@property (nonatomic, strong) NSNumber *trackingEnabled; +@end + +@implementation IDFAInfo +@end + +void getIDFAInfo(void (^completion)(IDFAInfo *)) { + // Show ATT prompt to get IDFA and updated ATT status + if (@available(iOS 14.5, *)) { + [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + IDFAInfo *info = [[IDFAInfo alloc] init]; + info.attStatus = status; + if (status == ATTrackingManagerAuthorizationStatusAuthorized) { + info.idfa = [[ASIdentifierManager sharedManager] + advertisingIdentifier]; + } + completion(info); + }); + }]; + // Don't show ATT prompt. Just get IDFA and tracking status. + } else { + ASIdentifierManager *manager = [ASIdentifierManager sharedManager]; + IDFAInfo *info = [[IDFAInfo alloc] init]; + info.trackingEnabled = @(manager.isAdvertisingTrackingEnabled); + if (manager.isAdvertisingTrackingEnabled) { + info.idfa = manager.advertisingIdentifier; + } + completion(info); + } +} + +void requestTrackingAuthorization(void (^completion) + (ATTrackingManagerAuthorizationStatus)) { + ATTrackingManagerAuthorizationStatus trackingStatus = + ATTrackingManager.trackingAuthorizationStatus; + + switch (trackingStatus) { + // Only show ATT prompt if status isn't determined + case ATTrackingManagerAuthorizationStatusNotDetermined: + // If app state is active, show ATT prompt + if ([UIApplication sharedApplication].applicationState == + UIApplicationStateActive) { + [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + completion(status); + }]; + // Wait until app state is active, then show ATT prompt + } else { + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *note) { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationDidBecomeActiveNotification + object:nil]; + [ATTrackingManager + requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + completion(status); + }]; + }]; + } + break; + // For all other statuses, return existing status + default: + completion(trackingStatus); + break; + } +} + +// Usage example +getIDFAInfo(^(IDFAInfo *info) { + // Include IDFA if available + if (info.idfa) { + params[@"idfa"] = [info.idfa UUIDString]; + } + + // Include either ATT status or tracking status, never both + if (info.attStatus != nil) { + params[@"att_status"] = [NSString stringWithFormat:@"%ld", + (long)info.attStatus]; + } else if (info.trackingEnabled != nil) { + params[@"tracking_enabled"] = [info.trackingEnabled boolValue] ? + @"1" : @"0"; + } +}); +``` + + + + +##### IDFV + +The ID for Vendors (IDFV) is a backup identifier available on all modern iOS devices. + + + + +```swift +let idfv: UUID? = UIDevice.current.identifierForVendor + +if let idfvString = idfv?.uuidString { + params["idfv"] = idfvString +} +``` + + + + +```objc +NSUUID *idfv = [[UIDevice currentDevice] identifierForVendor]; + +if (idfv) { + params[@"idfv"] = [idfv UUIDString]; +} +``` + + + + +##### Primary deduplication token + +To consistently measure app activities across uninstalls and reinstalls, generate a random version 4 UUID (the "primary deduplication token") and save it in the iOS keychain. The primary deduplication token is a backup identifier that you should generate for all devices. + + + + +```swift +import Foundation +import Security + +// App's bundle ID +let bundleId = "com.example.app" + +// Collect the primary dedupe token from the keychain +func getPrimaryDedupeToken(bundleId: String) -> UUID? { + // Define the query to search for the token in the keychain + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "primary_dedupe_token", + kSecAttrService as String: bundleId, + kSecReturnData as String: true + ] + + var item: CFTypeRef? + // Attempt to fetch the token from the keychain + let status = SecItemCopyMatching(query as CFDictionary, &item) + + // If the fetch was successful, convert the result to a UUID + guard status == errSecSuccess, + let existingItem = item as? Data, + let uuidString = String(data: existingItem, encoding: .utf8), + let token = UUID(uuidString: uuidString) else { + // Return nil if the token doesn't exist or couldn't be collected + return nil + } + + return token +} + +// Save the primary dedupe token to the keychain +func setPrimaryDedupeToken(_ token: UUID, bundleId: String) -> Bool { + let tokenData = token.uuidString.data(using: .utf8)! + // Define the attributes for storing the token in the keychain + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "primary_dedupe_token", + kSecAttrService as String: bundleId, + kSecValueData as String: tokenData, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock + ] + + // Attempt to add the token to the keychain + let status = SecItemAdd(query as CFDictionary, nil) + + // Return true if the token was successfully added, false otherwise + return status == errSecSuccess +} + +// Collect the existing primary dedupe token or create a new one if it doesn't exist +func getOrCreatePrimaryDedupeToken() -> UUID { + // Try to collect an existing token + if let existingToken = getPrimaryDedupeToken(bundleId: bundleId) { + return existingToken + } else { + // If no token exists, generate a new one + let newToken = UUID() + // Attempt to save the new token + if setPrimaryDedupeToken(newToken, bundleId: bundleId) { + return newToken + } else { + // If saving fails, throw a fatal error + fatalError("Failed to save primary dedupe token") + } + } +} + +// Usage example +let primaryDedupeToken = getOrCreatePrimaryDedupeToken() + +// Convert to lowercase string +params["primary_dedupe_token"] = primaryDedupeToken.uuidString.lowercased() +``` + + + + +```objc +#import +#import + +// App's bundle ID +NSString *const bundleId = @"com.example.app"; + +// Collect the primary dedupe token from the keychain +NSUUID *getPrimaryDedupeToken(NSString *bundleId) { + // Define the query to search for the token in the keychain + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount: @"primary_dedupe_token", + (__bridge id)kSecAttrService: bundleId, + (__bridge id)kSecReturnData: @YES + }; + + CFTypeRef item = NULL; + // Attempt to fetch the token from the keychain + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, + &item); + + // If the fetch was successful, convert the result to a UUID + if (status == errSecSuccess && item != NULL) { + NSData *existingItem = (__bridge_transfer NSData *)item; + NSString *uuidString = [[NSString alloc] + initWithData:existingItem encoding:NSUTF8StringEncoding]; + return [[NSUUID alloc] initWithUUIDString:uuidString]; + } + + // Return nil if the token doesn't exist or couldn't be collected + return nil; +} + +// Save the primary dedupe token to the keychain +BOOL setPrimaryDedupeToken(NSUUID *token, NSString *bundleId) { + NSData *tokenData = [[token UUIDString] + dataUsingEncoding:NSUTF8StringEncoding]; + // Define the attributes for storing the token in the keychain + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount: @"primary_dedupe_token", + (__bridge id)kSecAttrService: bundleId, + (__bridge id)kSecValueData: tokenData, + (__bridge id)kSecAttrAccessible: + (__bridge id)kSecAttrAccessibleAfterFirstUnlock + }; + + // Attempt to add the token to the keychain + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); + // Return YES if the token was successfully added, NO otherwise + return status == errSecSuccess; +} + +// Collect the existing primary dedupe token or create a new one if it doesn't exist +NSUUID *getOrCreatePrimaryDedupeToken(void) { + // Try to collect an existing token + NSUUID *existingToken = getPrimaryDedupeToken(bundleId); + if (existingToken) { + return existingToken; + } else { + // If no token exists, generate a new one + NSUUID *newToken = [NSUUID UUID]; + // Attempt to save the new token + if (setPrimaryDedupeToken(newToken, bundleId)) { + return newToken; + } else { + // If saving fails, throw an exception + @throw [NSException exceptionWithName:@"TokenSaveError" + reason:@"Failed to save primary dedupe token" + userInfo:nil]; + } + } +} + +// Usage example +NSUUID *primaryDedupeToken = getOrCreatePrimaryDedupeToken(); + +// Convert to lowercase string +params[@"primary_dedupe_token"] = [[primaryDedupeToken UUIDString] + lowercaseString]; +``` + + + + +#### [Google Play device IDs (Android)](google-play-device-ids-android) + +##### Google Advertising ID + +The Google Play Services Advertising ID (GPS ADID) is available on Android devices with Google Play Services, provided the user hasn't opted to delete their advertising ID. + + + +You can check for the most recent version of the Play Services Ads Identifier library on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-ads-identifier). + + + +1. Add the `play-services-ads-identifier` dependency to your app's `build.gradle` file: + + + + + + +```kotlin +dependencies { + implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") +} +``` + + + + + + + + +```groovy +dependencies { + implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0' +} +``` + + + + + + +2. Add the following permission to your `AndroidManifest.xml` file: + + + + ```xml + + ``` + + + +3. If your app uses R8 or ProGuard, add these rules to your `proguard-rules.pro` file to preserve classes and methods needed for Google Advertising ID retrieval during code optimization (create the file in your app module's directory if it doesn't exist): + + + + ```java + -keep class com.google.android.gms.common.ConnectionResult { + int SUCCESS; + } + -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { + com.google.android.gms.ads.identifier. + AdvertisingIdClient$Info getAdvertisingIdInfo( + android.content.Context); + } + -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { + java.lang.String getId(); + boolean isLimitAdTrackingEnabled(); + } + ``` + + + +4. Implement the code to collect Google Advertising ID and tracking status: + + + + +```kotlin +import com.google.android.gms.ads.identifier.AdvertisingIdClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun getGoogleAdvertisingIdInfo( + context: Context): AdvertisingIdClient.Info? { + return withContext(Dispatchers.IO) { + try { + AdvertisingIdClient.getAdvertisingIdInfo(context) + } catch (e: Exception) { + // Handle exceptions + // (for example: Google Play Services not available) + null + } + } +} + +// Usage example +// As getGoogleAdvertisingIdInfo is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { + val adInfo = getGoogleAdvertisingIdInfo(context) + adInfo?.let { info -> + // Include Google Advertising ID if tracking is not limited + if (!info.isLimitAdTrackingEnabled) { + params["gps_adid"] = info.id + } + // Set tracking status + params["tracking_enabled"] = if (info.isLimitAdTrackingEnabled) + "0" else "1" + } +} +``` + + + + +##### App Set ID + +App Set ID is a backup identifier available on all Android devices with Google Play Services installed and running API Level 30 (Android 11) or later. + + + +You can check for the most recent version of the Play Services App Set library on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-appset). + + + +1. Add the necessary dependency to your app's `build.gradle` file: + + + + + + +```kotlin +dependencies { + implementation("com.google.android.gms:play-services-appset:16.1.0") +} +``` + + + + + + + + +```groovy +dependencies { + implementation 'com.google.android.gms:play-services-appset:16.1.0' +} +``` + + + + + + +2. Implement the code to collect App Set ID: + + + + +```kotlin +import com.google.android.gms.appset.AppSet +import com.google.android.gms.appset.AppSetIdClient +import com.google.android.gms.appset.AppSetIdInfo +import com.google.android.gms.tasks.Tasks +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun getAppSetId(context: Context): String? { + return withContext(Dispatchers.IO) { + try { + val client: AppSetIdClient = AppSet.getClient(context) + val taskResult = Tasks.await(client.appSetIdInfo) + taskResult.id + } catch (e: Exception) { + // Handle exceptions (for example: Google Play Services not available) + null + } + } +} + +// Usage example +// As getAppSetId is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { + val appSetId = getAppSetId(context) + appSetId?.let { id -> + val params = mutableMapOf() + params["google_app_set_id"] = id + } +} +``` + + + + +## Additional parameters + +These parameters aren't required. If you use any of these parameters, you should include them in all requests. + +### Unix timestamp + +Including a Unix timestamp with each S2S request improves attribution accuracy by providing the time at which the activity occured on the device. + + + + +```swift +// Unix timestamp of when activity occurred on device +// Code example shows how to retrieve current time in seconds +// Example value: "1484085154" +params["created_at_unix"] = String(Int(Date().timeIntervalSince1970)) +``` + + + + +```kotlin +// Unix timestamp of when activity occurred on device +// Code example shows how to retrieve current time in seconds +// Example value: "1484085154" +params["created_at_unix"] = (System.currentTimeMillis() / 1000).toString() +``` + + + + +### Probabilistic modeling data points + +To use probabilistic matching as an attribution method, you need to include the following parameters on all S2S requests: + + + +| Parameter | Description | +| ------------- | ---------------------------------------------------------- | +| `device_name` | The name of the device. | +| `device_type` | The device type or model. | +| `os_version` | The version of the operating system running on the device. | +| `ip_address` | The IP address of the device | + +
+ +Adjust strongly recommends adding these parameters as it enables more comprehensive attribution, particularly for iOS. + + + + +```swift +import UIKit + +// Device name +// Example value: "iPhone10,5" +// Device name +// Example value: "iPhone10,5" +var systemInfo = utsname() +uname(&systemInfo) +let machineMirror = Mirror(reflecting: systemInfo.machine) +let deviceName = machineMirror.children.reduce("") { + identifier, element in + guard let value = element.value as? Int8, value != 0 + else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) +} +params["device_name"] = deviceName + +// Device type +// Example value: "iPhone" +params["device_type"] = UIDevice.current.model + +// OS version +// Example value: "17.5.1" +params["os_version"] = UIDevice.current.systemVersion + +// IP address +// Retrieve the device's IP address from requests to your server +params["ip_address"] = "192.0.0.1" // Example value +``` + + + + +```kotlin +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import android.content.pm.PackageManager + +// Usage example +val context: Context = // ... get your context here ... + +val isGooglePlayGamesForPC = context.packageManager + .hasSystemFeature("com.google.android.play.feature + .HPE_EXPERIENCE") + +// Device name +params["device_name"] = if (isGooglePlayGamesForPC) null + else Build.MODEL + +// OS version +params["os_version"] = if (isGooglePlayGamesForPC) null + else Build.VERSION.RELEASE + +// Device type +params["device_type"] = when { + isGooglePlayGamesForPC -> "pc" + (context.resources.configuration.uiMode and + Configuration.UI_MODE_TYPE_MASK) == Configuration + .UI_MODE_TYPE_TELEVISION -> "tv" + else -> when (context.resources.configuration.screenLayout + and Configuration.SCREENLAYOUT_SIZE_MASK) { + Configuration.SCREENLAYOUT_SIZE_SMALL, + Configuration.SCREENLAYOUT_SIZE_NORMAL -> "phone" + Configuration.SCREENLAYOUT_SIZE_LARGE, + Configuration.SCREENLAYOUT_SIZE_XLARGE -> "tablet" + else -> null + } +} + +// IP address +// Retrieve the device's IP address from requests to +// your server +params["ip_address"] = "192.0.0.1" // Example value +``` + + + + +### Environment + +You can specify the environment that requests are being sent in by passing an `environment` parameter. Requests from different environments are kept separate in Adjust to enable testing. The following values are available: + +- `sandbox`: use this while testing to keep your requests separate from your production data. +- `production`: use this when you release your app. + +If you don't pass this parameter, the default value is `production`. + + + + +```swift +// For testing (sandbox environment) +params["environment"] = "sandbox" + +// For production use +params["environment"] = "production" +``` + + + + +```kotlin +// For testing (sandbox environment) +params["environment"] = "sandbox" + +// For production use +params["environment"] = "production" +``` + + + + +### Global callback parameters + +When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include "global callback parameters" in all your S2S requests to add custom parameters to the raw data. This is commonly used to include your internal user ID in your exported raw data. + +Global callback parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value"}' +``` + + + + +```kotlin +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value"}' +``` + + + + +### Global partner parameters + +When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. Adjust's servers passes these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. + +Global partner parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_visitor_id": "nzFC9LKSqM"}' +``` + + + + +```kotlin +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_visitor_id": "nzFC9LKSqM"}' +``` + + + + +## Requests + + + +Ensure all parameter values are URL encoded before sending requests. + + + +### Session + +Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. Adjust's servers log successful session requests as follows: + +- It records the first session for a device as an "install" activity. +- It records subsequent sessions as "session" activities. +- It records a "reattribution" or "reattribution reinstall" activity if [reattribution criteria](https://help.adjust.com/en/article/reattribution) are satisfied. + + + + For reference, the Adjust SDK sends a session request to Adjust's servers when either of the following occurs: + + - The user opens the app for the first time after install or reinstall. + - The user reopens the app after it has been closed or in the background for at least 30 minutes. + + + + When sending S2S session requests with the `created_at_unix` parameter, Adjust's servers require this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. + + + +```sh +curl -X POST "https://app.adjust.com/session" \ -H "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type: application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=i9dukg8o5slc\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D"\ +-w "\n\nHTTP Status Code: %{http_code}\n"\ +-s +``` + + + +This is the response format when Adjust successfully logs the first session for the device. You can use the Adjust [testing console](https://help.adjust.com/en/article/testing-console) to forget your device and test this multiple times, if needed. + + + +```console +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T01:31:14.373Z+0000", + "message": "Install tracked", + "ask_in": 2000 +} + +HTTP Status Code: 200 +``` + + + +This is the response format when Adjust successfully logs subsequent sessions for the device. + + + +```console +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T02:31:14.373Z+0000", + "message": "Session tracked", + "ask_in": 5000 +} + +HTTP Status Code: 200 +``` + + + +### Attribution + +After sending a session request, you may need to send an attribution request to Adjust's servers. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. + +Attribution requests serve three main purposes for developers: + +- Obtain the attribution source for installs or reinstalls, allowing you to personalize the user experience. +- Retrieve deferred deep links for users who clicked an Adjust deep link before installation. +- Get updated attribution information in case of reattribution. + +There are two options for sending attribution requests: + +- Send after every session (covers all use cases) +- Send only after install or reinstall (covers the first two use cases, which are most common) + +To implement the second option, follow these steps: + +1. Create a flag in your app. +2. Use this flag to determine if the first session has been recorded. +3. Send the attribution request only when this flag indicates it's the first session. + + + + +```swift +import Foundation + +// Helps to manage app-wide persistent settings and states +class UserDefaultsManager { +static let shared = UserDefaultsManager() +private let userDefaults = UserDefaults.standard + + // ... + + private let FIRST_SESSION_RECORDED_KEY = "first_session_recorded" + + var firstSessionRecorded: Bool { + get { + return userDefaults.bool(forKey: FIRST_SESSION_RECORDED_KEY) + } + set { + userDefaults.set(newValue, forKey: FIRST_SESSION_RECORDED_KEY) + } + } + + // ... + +} + +// Usage example +let userDefaultsManager = UserDefaultsManager.shared + +if !userDefaultsManager.firstSessionRecorded { +// Send first session request +// If the request is successful: +userDefaultsManager.firstSessionRecorded = true +// Wait according to "ask_in" time +// Send attribution request for first session +} else { +// Send subsequent session requests +} + +``` + + + + +```kotlin +import android.content.Context +import android.content.SharedPreferences + +/* Helps to manage app-wide persistent settings and states */ +class SharedPreferencesManager private constructor(context: Context) { +private val sharedPreferences: SharedPreferences = +context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + companion object { + private const val PREFS_NAME = "AppPrefs" + private const val FIRST_SESSION_RECORDED_KEY = "first_session_recorded" + + // ... + + @Volatile + private var instance: SharedPreferencesManager? = null + + fun getInstance(context: Context): SharedPreferencesManager { + return instance ?: synchronized(this) { + instance ?: SharedPreferencesManager(context.applicationContext).also { + instance = it + } + } + } + } + + var firstSessionRecorded: Boolean + get() = sharedPreferences.getBoolean(FIRST_SESSION_RECORDED_KEY, false) + set(value) = sharedPreferences.edit().putBoolean( + FIRST_SESSION_RECORDED_KEY, value).apply() + + // ... + +} + +// Usage example + +val userDefaultsManager = UserDefaultsManager.getInstance(context) + +if (!userDefaultsManager.firstSessionRecorded) { +// Send first session request +// If the request is successful: +userDefaultsManager.firstSessionRecorded = true +// Wait according to "ask_in" time +// Send attribution request for first session +} else { +// Send subsequent session requests +} + +``` + + + + +Send an attribution request. + + + +```sh +curl -X POST "https://app.adjust.com/attribution" \ +-H "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type:application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=4w565xzmb54d\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D"\ +-w "\n\nHTTP Status Code: %{http_code}\n" \ +-s +``` + + + +Below is an example attribution response that contains a deferred deep link. + + + +```json +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-10T23:48:27.244Z+0000", + "message": "Attribution found", + "attribution": { + "tracker_token": "18msd3tn", + "tracker_name": "Test", + "network": "Test", + "deeplink": "example://summer-clothes?promo=beach\u0026adj_t=18msd3tn\u0026adjust_no_sdkclick=1" + } +} +``` + + + +Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that Adjust's servers haven't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. + +If the attribution response contains a deferred deep link, here is a suggested approach to handle it: + +1. Store the deep link persistently in the app (for example: in `UserDefaults` for iOS or `SharedPreferences` for Android) for later use. This is often necessary because many apps have onboarding screens and login processes that need to be completed before handling the deferred deep link. + +2. When ready to process the deep link, do the following: + - Retrieve and parse the stored link. + - Implement domain-agnostic handling to treat all links equivalently: + - Adjust universal links (for example: `example.go.link`) + - Other universal links (for example: `example.com`) + - App scheme deep links (for example: `example://`) + - Extract relevant information (path, query parameters, fragment) regardless of link type. + - Navigate to the deep link screen based on the extracted information. + +Example equivalence: + +- `https://example.go.link/summer-clothes?promo=beach` +- `https://example.com/summer-clothes?promo=beach` +- `example://summer-clothes?promo=beach` + +These should all lead to the same destination in your app. + + + +Adjust's servers returns all deferred deep links in app scheme format (for example: `example://`), regardless of their original format. + + + +Here's a reference implementation demonstrating these concepts: + + + + +```swift +import Foundation +import UIKit + +/// Handles the storage and retrieval of the deferred deep link and onboarding status +class UserDefaultsManager { +static let shared = UserDefaultsManager() +private let userDefaults = UserDefaults.standard + + // ... + + private let DEFERRED_LINK_KEY = "deferredLink" + private let HAS_COMPLETED_ONBOARDING_KEY = "hasCompletedOnboarding" + + // ... + + func getDeferredLink() -> String? { + return userDefaults.string(forKey: DEFERRED_LINK_KEY) + } + + func setDeferredLink(_ value: String) { + userDefaults.set(value, forKey: DEFERRED_LINK_KEY) + } + + func removeDeferredLink() { + userDefaults.removeObject(forKey: DEFERRED_LINK_KEY) + } + + func getHasCompletedOnboarding() -> Bool { + return userDefaults.bool(forKey: HAS_COMPLETED_ONBOARDING_KEY) + } + + func setHasCompletedOnboarding(_ value: Bool) { + userDefaults.set(value, forKey: HAS_COMPLETED_ONBOARDING_KEY) + } + +} + +/// Demonstrates how to handle onboarding and deferred deep links +class ViewController: UIViewController { +let userDefaultsManager = UserDefaultsManager.shared + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !userDefaultsManager.getHasCompletedOnboarding() { + // Show onboarding screens and login + // On completion, set hasCompletedOnboarding to true + userDefaultsManager.setHasCompletedOnboarding(true) + } + + // Check if there's a stored deferred deep link + if let deferredLinkString = userDefaultsManager.getDeferredLink(), + let deferredLink = URL(string: deferredLinkString) { + // Remove the stored URL to avoid handling it again later + userDefaultsManager.removeDeferredLink() + // Handle deferred deep link + DeeplinkHandler.handleDeeplink(deferredLink, navigationController: self.navigationController) + } else { + // Show main content + } + } + +} + +/// Handles the logic for processing and navigating to deep links +class DeeplinkHandler { + static func handleDeeplink(_ incomingLink: URL, + navigationController: UINavigationController?) { + // Extract path, query items, and fragment from the link + let components = URLComponents(url: incomingLink, + resolvingAgainstBaseURL: true) + let path = components?.path ?? "" + let queryItems = components?.queryItems + let fragment = components?.fragment + + // Implement navigation or other app-specific + // logic based on the deep link components. + DispatchQueue.main.async { + if path == "/summer-clothes" { + let promoCode = queryItems?.first(where: { + $0.name == "promo" + })?.value + let summerClothesVC = SummerClothesViewController( + promoCode: promoCode) + navigationController?.pushViewController( + summerClothesVC, animated: true) + } + } + } + +} + +// Usage example when receiving the attribution response + +if let deeplink = attributionResponse["attribution"]["deeplink"] + as? String { + UserDefaultsManager.shared.setDeferredLink(deeplink) +} + +``` + + + + +```kotlin +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity + +/* Helps to manage app-wide persistent settings and states / +class SharedPreferencesManager private constructor(context: Context) { +private val sharedPreferences: SharedPreferences = +context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + companion object { + private const val PREFS_NAME = "AppPrefs" + + // ... + + private const val DEFERRED_LINK_KEY = "deferredLink" + private const val HAS_COMPLETED_ONBOARDING_KEY = "hasCompletedOnboarding" + + @Volatile + private var instance: SharedPreferencesManager? = null + + fun getInstance(context: Context): SharedPreferencesManager { + return instance ?: synchronized(this) { + instance ?: SharedPreferencesManager(context.applicationContext).also { + instance = it + } + } + } + } + + // ... + + fun getDeferredLink(): String? { + return sharedPreferences.getString(DEFERRED_LINK_KEY, null) + } + + fun setDeferredLink(value: String) { + sharedPreferences.edit().putString(DEFERRED_LINK_KEY, value).apply() + } + + fun removeDeferredLink() { + sharedPreferences.edit().remove(DEFERRED_LINK_KEY).apply() + } + + fun getHasCompletedOnboarding(): Boolean { + return sharedPreferences.getBoolean(HAS_COMPLETED_ONBOARDING_KEY, false) + } + + fun setHasCompletedOnboarding(value: Boolean) { + sharedPreferences.edit().putBoolean(HAS_COMPLETED_ONBOARDING_KEY, value).apply() + } + +} + +/* Demonstrates how to handle onboarding and deferred deep links */ +class MainActivity : AppCompatActivity() { +private lateinit var sharedPreferencesManager: SharedPreferencesManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + sharedPreferencesManager = SharedPreferencesManager.getInstance(this) + } + + override fun onResume() { + super.onResume() + + if (!sharedPreferencesManager.getHasCompletedOnboarding()) { + // Show onboarding screens and login + // On completion, set hasCompletedOnboarding to true + sharedPreferencesManager.setHasCompletedOnboarding(true) + } + + // Check if there's a stored deferred deep link + sharedPreferencesManager.getDeferredLink()?.let { deferredLinkString -> + // Remove the stored URL to avoid handling it again later + sharedPreferencesManager.removeDeferredLink() + // Handle deferred deep link + DeeplinkHandler.handleDeeplink(deferredLinkString, supportFragmentManager) + } ?: run { + // Show main content + } + } + +} + +/* Handles the logic for processing and navigating to deep links */ +object DeeplinkHandler { +fun handleDeeplink(incomingLink: String, fragmentManager: androidx.fragment.app.FragmentManager) { +// Extract path, query items, and fragment from the link +val uri = android.net.Uri.parse(incomingLink) +val path = uri.path ?: "" +val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } +val fragment = uri.fragment + + // Implement navigation or other app-specific + // logic based on the deep link components. + if (path == "/summer-clothes") { + val promoCode = queryParams["promo"] + val summerClothesFragment = SummerClothesFragment.newInstance(promoCode) + fragmentManager.beginTransaction() + .replace(R.id.fragment_container, summerClothesFragment) + .addToBackStack(null) + .commit() + } + } + +} + +// Usage example +attributionResponse["attribution"]?.get("deeplink")?.asString?.let { deeplink -> + SharedPreferencesManager.getInstance(context).setDeferredLink(deeplink) +} +``` + + + + +### Post-install event + +After you send at least one successful session request for a device, you can send [post-install events](/en/api/s2s-api/events). These are typically events that represent marketing goals, and that networks can use to optimize campaigns. + + + + +```swift +// Add event token to existing params +params["event_token"] = "2y7e81" + +// Add revenue and currency, if applicable +// These parameters are equivalent to $19.99 +params["revenue"] = "19.99" +params["currency"] = "USD" +``` + + + + +```kotlin +// Add event token to existing params +params["event_token"] = "2y7e81" + +// Add revenue and currency, if applicable +// These parameters are equivalent to $19.99 +params["revenue"] = "19.99" +params["currency"] = "USD" +``` + + + + +#### Callback parameters + +When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include custom "callback parameters" in specific event requests to add event-level custom data. For instance, on a purchase event, you might want to include your internal transaction ID in the raw data for that event. + +Callback parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift +// If callback_params exists, add the event callback parameters to it (for example: txn_id) +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value", "txn_id": "8837853376"}' + +// If callback_params does not exist, create it +params["callback_params"] = '{"txn_id": "8837853376"}' +``` + + + + +```kotlin +// If callback_params exists, add the event callback parameters to it (for example: txn_id) +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value", "txn_id": "8837853376"}' + +// If callback_params does not exist, create it +params["callback_params"] = '{"txn_id": "8837853376"}' +``` + + + + +#### Partner parameters + +When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. Adjust's servers will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like `view_item`, `add_to_cart`, and purchase. + +Partner parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift +// If partner_params exists, add the event partner parameters to it (for example: item_id) +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM", "item_id": "[\"76524\",\"62599\"]"}' + +// If partner_params does not exist, create it +params["partner_params"] = '{"item_id": "[\"76524\",\"62599\"]"}' +``` + + + + +```kotlin +// If partner_params exists, add the event partner parameters to it (for example: item_id) +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM", "item_id": "[\"76524\",\"62599\"]"}' + +// If partner_params does not exist, create it +params["partner_params"] = '{"item_id": "[\"76524\",\"62599\"]"}' +``` + + + + +Send an event request. + + + +```sh +curl -X POST "https://app.adjust.com/event" \ +-H "Authorization:Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type:application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=4w565xzmb54d\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%2C%20%22txn_id%22%3A%20%228837853376%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%2C%20%22item_id%22%3A%20%22%5B%5C%2276524%5C%22%2C%5C%2262599%5C%22%5D%22%7D"\ +&event_token=2y7e81\ +&revenue=19.99\ +¤cy=USD\ +-w "\n\nHTTP Status Code: %{http_code}\n"\ +-s +``` + + + + + +```console +{ + "status": "OK" +} + +HTTP Status Code: 200 +``` + + diff --git a/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx b/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx index 96de834c4..ac28756ea 100644 --- a/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx +++ b/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx @@ -2,7 +2,7 @@ title: "Transition SDK to S2S events" description: "You can change your setup to stop sending Adjust SDK events and instead send server-to-server (S2S) events." slug: en/api/s2s-api/sdk-to-s2s-events -sidebar-position: 6 +sidebar-position: 5 sidebar-label: Transition SDK to S2S events ---