# TapResearch Publisher Documentation — SDK — iOS # Source: https://supply-docs.tapresearch.com · generated at build time, do not edit by hand. # Each section below is one documentation page. # iOS Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/ios :::tip What you'll do 1. [Install the SDK](#installation) 2. [Set up reward callbacks](#callbacks) 3. [Initialize the SDK](#initialization) 4. [Display a placement](#displaying-a-placement) Later sections cover user attributes, the user identifier, placement details, custom parameters, and more. **Testing?** Your app starts in test mode, so you'll need a [test device](../../testing-and-qa) to see content while you build. ::: ## Installation __Example apps are available [here](https://github.com/TapResearch/demo-apps/tree/master/ios)__ ### CocoaPods To keep up to date with the latest version of the TapResearch iOS SDK, you can use CocoaPods to integrate the SDK into your project. ```ruby platform :ios, '13.0' source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyApp' do pod 'TapResearch', '~> 3.7.3' end ``` Then run in the Terminal: `pod install` ### Swift Package Manager Add the following URL in the Xcode Swift Package Manager: https://github.com/TapResearch/TapResearch-iOS-SDK * Set Dependency Rule to "Exact Version" or "Up to Next Major Version" * Set the version value to 3.7.3 * Set "Add to Project" to your project * Click "Add Package" In the resulting "Choose Package Product for TapResearch-iOS-SDK" select the TapResearchSDK row and click "Add Package". :::note Adding the package with an AI agent An agent running **outside** Xcode (a terminal or IDE agent) can add this Swift Package by editing your Xcode project file directly. An agent running **inside** Xcode (for example Xcode's built-in coding assistant) can't — Xcode holds the project file open, so it can't be modified while Xcode is running. In that case, add the package yourself with the steps above, then hand control back to the agent to continue. ::: ### Manual Installation You can download the latest version of the SDK from [our GitHub page](https://github.com/TapResearch/TapResearch-iOS-SDK/releases). > Minimum iOS version: 13.0 ## Integration ### Callbacks The TapResearch SDK has two delegates you can implement: `TapResearchSDKDelegate` and `TapResearchContentDelegate`. If you are implementing the delegates as their own objects make sure that the delegate objects derive from `NSObject`, for example in Swift: `class MyDelegates: NSObject, TapResearchSDKDelegate {`. #### TapResearchSDKDelegate - **`onTapResearchDidReceiveRewards`**: Called when a user has earned a reward(s). - **`onTapResearchQuickQuestionResponse`**: Called when a quick question result is being returned. - **`onTapResearchDidError`**: Called when an error has occurred in the TapResearch SDK. For the full list, see the [error codes page](./sdk-error-codes) - **`onTapResearchSdkReady`**: Called when the SDK is ready for use. **We recommend showing content only after this callback is called.** The class, ideally `SceneDelegate`, that conforms to the `TapResearchSDKDelegate` protocol must define both the `onTapResearchDidReceiveRewards` and `onTapResearchDidError` methods. Defining the `onTapResearchSdkReady` is highly recommended. ##### Reward object The reward object contains the following properties: * `transactionIdentifier` will be the unique identifier for the reward - this is used to prevent duplicate rewards * `placementIdentifier` will be the unique identifier for the placement - this can be used by your analytics to show which placement rewarded the user * `currencyName` will be the name of the currency name that was rewarded. This is the `Currency Name` that you set up in the dashboard. This is different from the `Display Name` * `rewardAmount` will be the amount of the currency that was rewarded, in your currency * `placementTag` will be the tag of the placement that rewarded the user - its human readable and unique ##### Quick Question Response New to Quick Questions? See [Quick Questions](../../features/quick-questions) for what they are and the response payload. A new delegate is available in **TapResearchSDKDelegate**: - **`onTapResearchQuickQuestionResponse`**: Called when a Quick Question response is being returned. This callback will return a **TRQQDataPayload** object. Swift in SwiftUI You can use a class to implement the SDK's delegates and pass that to TapResearch at initialization. ```swift class TapResearchSDKDelegates : TapResearchSDKDelegate { func onTapResearchDidError(_ error: NSError) { print("\(#function): \(error.code), \(error.localizedDescription)") } func onTapResearchDidReceiveRewards(_ rewards: [TRReward]) { print("\(#function): \(rewards.count) rewards awarded") var i: Int = 0 for reward in rewards { print("reward \(i): currency = \(reward.currencyName ?? "unknown"), amount = \(reward.rewardAmount)") i += 1 } } func onTapResearchQuickQuestionResponse(_ qqPayload: TRQQDataPayload) { for question: TRQQDataPayloadQuestion in qqPayload.questions { //... } } func onTapResearchSdkReady() { if let error: NSError = TapResearch.sendUserAttributes(attributes: ["attribute1" : "some player attribute", "a_number" : 12]) { print("sendUserAttributes: \(error.code) \(error.localizedDescription)") } print("\(#function)") } } ``` Swift in UIKit ```swift class SomeClass: TapResearchSDKDelegate { func onTapResearchDidReceiveRewards(_ rewards: [TRReward]) { print("onTapResearchDidReceiveRewards(rewards...)") } func onTapResearchDidError(_ error: NSError) { print("onTapResearchDidError: \(error.userInfo[NSError.TapResearchErrorCodeString] ?? "(No code)") \(error.localizedDescription)") } func onTapResearchQuickQuestionResponse(_ qqPayload: TRQQDataPayload) { for question: TRQQDataPayloadQuestion in qqPayload.questions { //... } } func onTapResearchSdkReady() { if let error: NSError = TapResearch.sendUserAttributes(attributes: ["attribute1" : "some player attribute", "a_number" : 12]) { print("sendUserAttributes: \(error.code) \(error.localizedDescription)") } print("onTapResearchSdkReady()") } } ``` ```objectivec @interface SceneDelegate () ... - (void)onTapResearchDidReceiveRewards:(NSArray *)rewards { NSLog(@"onTapResearchDidReceiveRewards(rewards...)"); } - (void)onTapResearchQuickQuestionResponse:(TRQQDataPayload *)qqPayload { NSLog(@"[%@] onTapResearchQuickQuestionResponse(%@)", NSDate.now.description, qqPayload); } - (void)onTapResearchDidError:(NSError *)error { NSLog(@"onTapResearchDidError: %@ %@", [error.userInfo[NSErrorTapResearchErrorCodeString] ?: @"(No code)", error.localizedDescription]); } - (void)onTapResearchSdkReady { NSLog(@"[%@] onTapResearchSdkReady()", NSDate.now.description); } ``` #### TapResearchRewardDelegate - **`onTapResearchDidReceiveRewards`**: Called when a user has earned a reward(s). The reward delegate that is defined in `TapResearchSDKDelegate` is optional and can be implemented or not when initializing the SDK. If you implement a reward handler on your TapResearchSDKDelegate object then rewards can be sent to your app as soon as the SDK is ready, if not then your app will not receive any rewards until a reward delegate has been set. ##### Setting a reward delegate To set a reward handler when you are ready to receive rewards you can use: ``` TapResearch.setRewardDelegate(_ delegate: TapResearchRewardDelegate?) ``` ``` [TapResearch setRewardDelegate:(id _Nullable)]; ``` The object you pass in must conform to `TapResearchRewardDelegate` and implement `onTapResearchDidReceiveRewards`. When you no longer need the reward delegate you can simply pass `nil` to the same SDK function. :::warning If you set the reward delegate from a view controller, for example in `viewDidLoad`, make sure to call `TapResearch.setRewardDelegate(nil)` when the view controller is no longer in the stack. ::: :::note Anytime you set a new reward delegate, or nil, it replaces the previously set value. For example: if you have a reward delegate available at init time then later set another reward delegate, the original delegate will be replaced by the new one. ::: #### TapResearchQuickQuestionDelegate - **`onTapResearchQuickQuestionResponse`**: Called when a quick question result is being returned. To set a Quick Questions response handler when you are ready to receive rewards you can use: ``` TapResearch.setQuickQuestionDelegate(_ delegate: TapResearchQuickQuestionDelegate?) ``` ``` [TapResearch setQuickQuestionDelegate:(id _Nullable)]; ``` The object you pass in must conform to `TapResearchQuickQuestionDelegate` and implement `onTapResearchQuickQuestionResponse`. When you no longer need the Quick Questions response delegate you can simply pass `nil` to the same SDK function. :::warning If you set the Quick Questions response delegate from a view controller, for example in `viewDidLoad`, make sure to call `TapResearch.setQuickQuestionDelegate(nil)` when the view controller is no longer in the stack. ::: :::note Anytime you set a new Quick Questions response delegate, or nil, it replaces the previously set value. For example: if you have a Quick Questions response delegate available at init time then later set another Quick Questions response delegate, the original delegate will be replaced by the new one. ::: #### TapResearchContentDelegate - `onTapResearchContentShown`: Called when content is successfully presented to the user. - `onTapResearchContentDismissed`: Called when content is dismissed. You can use these callbacks to pause/resume audio, animations, etc. when content is shown or dismissed. Swift in SwiftUI ```swift title="ViewController.swift" class SomeClass: TapResearchContentDelegate { //... func onTapResearchContentShown(forPlacement placement: String) { print("\(#function): \(placement) was shown") } func onTapResearchContentDismissed(forPlacement placement: String) { print("\(#function): \(placement) was dismissed") } //... } ``` Swift in UIKit ```swift title="ViewController.swift" class TapResearchDelegates: TapResearchContentDelegate { //... func onTapResearchContentShown(forPlacement placement: String) { print("\(#function): \(placement) was shown") } func onTapResearchContentDismissed(forPlacement placement: String) { print("\(#function): \(placement) was dismissed") } //... } ``` ```objectivec title="ViewController.m" @interface SomeClass () ... - (void)onTapResearchContentShownForPlacement:(NSString *)placement { NSLog(@"onTapResearchContentShown(%@)", placement); } - (void)onTapResearchContentDismissedForPlacement:(NSString *)placement { NSLog(@"onTapResearchContentDismissed(%@)", placement); } ``` ### Initialization > We recommend initializing TapResearch as early as possible in your application's lifecycle. > This will ensure that your placements are refreshed by the time you are ready to show content. To initialize the SDK, you'll need the API token attached to the app you set up in your TapResearch dashboard. Refer to the [app setup guide](../../setup.md#4-configure-app-settings) for more information. After obtaining your API Token, add the following snippet to the `scene:willConnectToSession:options:` method in your `SceneDelegate` class. Replace `API_TOKEN` with your API token and `UNIQUE_USER_ID` with a unique identifier within your application. :::warning Don't put the token in Info.plist Keep your API token and placement tag in a **separate Swift config file** (for example `TapResearchCredentials.swift`) and pass them to `initialize`. Do **not** add them to `Info.plist` — TapResearch never reads them from there, and on SwiftUI projects where `Info.plist` is auto-generated, hand-creating one to hold credentials will break your build. The token is only ever used in the `initialize` call. ::: If initializing the TapResearch SDK isn't possible inside the `scene:willConnectToSession:options:` method, try and do it as early as possible to ensure that your placements are refreshed by the time you are ready to show content. :::tip The user identifier passed through initialization must be **unique**! Only initialize once you are able to provide a unique identifier. Re-using the same user identifier is not supported and will prevent your users from accessing our service. ::: Swift in UIKit ```swift title="SceneDelegate.swift" func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } TapResearch.initialize(withAPIToken: apiToken, userIdentifier: userIdentifier, sdkDelegate: self) { (error: NSError?) in if let e = error { print(e.localizedDescription as Any) } } } ``` Swift in SwiftUI :::info Initialize the SDK in the `init()` method of your root view. ::: ```swift title="MainContentView.swift" let tapSDKDelegates: TapResearchSDKDelegates init() { tapSDKDelegates = TapResearchSDKDelegates() TapResearch.initialize(withAPIToken: apiToken, userIdentifier: userId, sdkDelegate: self.tapSDKDelegates) { (error: NSError?) in if let error = error { print("\(#function): \(error.code), \(error.localizedDescription)") } } } ``` ```objectivec title="SceneDelegate.m" - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). NSError *error; [TapResearch initializeWithAPIToken:API_TOKEN userIdentifier:UNIQUE_USER_ID sdkDelegate:self error:&error]; if (error) { NSLog(@"%@", error.localizedDescription); } } ``` ### Send User attributes > **See more on user attributes [here](../../operational-tools/user-attributes).** User attributes are used to target specific users with content (ex: an offer). They can be set at any time and the most recent values received will be used when determining which content to show. User attributes are passed as a dictionary of key-value pairs. :::note User attributes prefixed with `tapresearch_` are reserved for internal use. Please do not use this prefix for user attributes as doing so will result in an error ::: The keys must be strings and the values must be one of: * String * Float * Integer > If you want to use a date, please stringify an ISO8601 date or use a timestamp. You can send user attributes as soon as the SDK is ready. This is done by calling `sendUserAttributes` with a `Dictionary` of attributes. We suggest sending as many attributes as you think that you'll want to target on for surveys, special awards, etc. An optional flag `clearPreviousAttributes` can be passed, set to `true` to clear previously-set attributes, the default is `false`. ```swift title="ViewController.swift" func sendUserAttributes() { let date = Date() // Current date and time let formatter = ISO8601DateFormatter() let iso8601String = formatter.string(from: date) let dict: [AnyHashable:Any] = ["app-user": userIdentifier, "roller-type": "high", "roller-type-valid-until": Date().timeIntervalSince1970 + ViewController.threeHours, "some-date": iso8601String, "some-double": 1.11] if let error = TapResearch.sendUserAttributes(attributes: dict, clearPreviousAttributes: true) { print(error.localizedDescription as Any) } } // ... ``` ```objectivec title="ViewController.m" // ... - (void)sendUserAttributes { NSDate *currentDate = [NSDate date]; NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init]; NSString *iso8601String = [formatter stringFromDate:currentDate]; NSTimeInterval threeHours = 3 * 60 * 60; NSNumber *rollerTypeValidUntil = @([currentDate timeIntervalSince1970] + threeHours); NSNumber *someDouble = @1.11; NSDictionary *dict = @{ @"app-user": userIdentifier, @"roller-type": @"high", @"roller-type-valid-until": rollerTypeValidUntil, @"some-date": iso8601String, @"some-double": someDouble }; NSError *error = [TapResearch sendUserAttributesWithAttributes:dict clearPreviousAttributes:YES]; if (error) { NSLog(@"%@", [error localizedDescription]); } } // ... // ``` You can also pass user attributes when you first initialize the SDK. This is done by changing the `TpResearch.initialize(...)` to additionally pass a `userAttributes` dictionary and `clearPreviousAttributes` flag. :::info If user attributes are known at SDK initialization, it is preferable to pass them through `initialize` compared to using `sendUserAttributes`. This will result in quicker load times for targeted content. ::: ```swift let dict: [AnyHashable:Any] = ["app-user": userIdentifier, "roller-type": "high", "roller-type-valid-until": Date().timeIntervalSince1970 + ViewController.threeHours] TapResearch.initialize(withAPIToken: apiToken, userIdentifier: userIdentifier, userAttributes: dict, clearPreviousAttributes: true, sdkDelegate: self) { (error: Error?) in if let error = error { print(error.localizedDescription as Any) } } ``` ```objectivec NSDictionary *dict = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"a string", @1, nil] forKeys:[NSArray arrayWithObjects:@"string", @"number", nil]]; [TapResearch initializeWithAPIToken:apiToken userIdentifier:userIdentifier u serAttributes:dict clearPreviousAttributes:YES sdkDelegate:self completion:^(NSError * _Nullable error) { if (error) { NSLog(@"Error on initialize: %ld, %@", (long)error.code, error.localizedDescription); } }]; ``` ### Setting the user identifier You can set the user identifier at any time. This is done by calling `setUniqueUserIdentifier` with a string. ```swift TapResearch.setUserIdentifier("some-user-id") ``` ```objectivec [TapResearch setUserIdentifier: @"some-user-id"]; ``` ### Checking if the SDK is ready The SDK lets the app know it is ready using a callback: ```swift func onTapResearchSdkReady() { } ``` ```objectivec - (void)onTapResearchSdkReady { } ``` You can also check if the SDK is ready using the `isReady` call: ```swift let ready: Bool = TapResearchSDK.isReady() ``` ```objectivec BOOL ready = [TapResearchSDK isReady]; ``` ### Getting Placement Details :::info `getPlacementDetails` is available from v3.6.3 ::: :::warning Note! Placement details can change as our SDK refreshes placements in the background, so it is not recommended to store or cache these values. Instead, refetch the details each time you need to use them, e.g. to display something in your UI. ::: To get details for a placement call `getPlacementDetails`: ```swift let details: TRPlacementDetails? = TapResearch.getPlacementDetails("your-placement-tag") { (error: NSError?) in // Handle error } if let details { // Use the placement details } ``` ```swift TRPlacementDetails *details = [TapResearch getPlacementDetails:@"your-placement-tag" errorHandler:^(NSError * _Nullable error) { if (error) { // Handle the error } }]; if (details) { // Ues the placement details } ``` #### The `TRPlacementDetails` objects: ```swift @objc public final class TRPlacementDetails : NSObject, Encodable { @objc public let name : String @objc public let contentType : String @objc public let currencyName : String @objc public let isSale : Bool @objc public let saleType : String? @objc public let saleEndDate : String? @objc public let saleMultiplier : Double @objc public let saleDisplayName : String? @objc public let saleTag : String? @objc public let bonusBarProgress : TRBonusBarProgress? } @objc public final class TRBonusBarProgress : NSObject, Encodable { @objc public let isActive : Bool @objc public let currentCompletes : Int @objc public let bonusWindowEndAt : String? @objc public let bonusTiers : [TRBonusTier]? @objc public let error : NSError? } @objc public final class TRBonusTier : NSObject, Encodable { @objc public let tierNumber : Int @objc public let completesNeeded : Int @objc public let rewardAmount : Int @objc public let status : String? } ``` ### Displaying a placement Placements are locations and moments inside the game when content can be shown. To show content for a placement, you'll need to specify a placement `tag`. Note that the show content calls are wrapped in a `canShowContent` check. This is to ensure that the placement is ready to be shown. If the placement is not ready, the SDK will return `false`. This helps guard against showing the user something that is not ready to be shown yet. If there is an error while showing a placement, it will be returned in the completion handler. The error will contain a code and a description. Placement tags can be found in your TapResearch dashboard on the App Settings page. Swift in UIKit ```swift title="ViewController.swift" if TapResearch.canShowContent(forPlacement: text) { let customParameters = ["param1": 123, "param2": "abc"] as [String : Any] TapResearch.showContent(forPlacement: text, delegate: self, customParameters: customParameters) { (error: NSError?) in if let e = error { print("\(e.userInfo[NSError.TapResearchErrorCodeString] ?? "(No code)") \(e.localizedDescription)") } } } ``` `canShowContent` now accepts an error response block, this is optional in Swift and required in Objective-C. ```swift let canShow: Bool = TapResearch.canShowContent(forPlacement: placementTag) { (error: NSError?) in // Handle error if needed } ``` ```objectivec BOOL canShow = [TapResearch canShowContentForPlacement:placementTag error:^(NSError * _Nullable error) { // Handle error if needed }]); ``` Swift in SwiftUI ```swift title="ContentView.swift" if TapResearch.canShowContent(forPlacement: placementTag) { let customParameters = ["param1": 123, "param2": "abc"] as [String : Any] TapResearch.showContent(forPlacement: placementTag, delegate: self.tapResearchDelegates, customParameters: customParameters) } else { print("Placement \(placementTag) not ready") } ``` ```objectivec title="ViewController.m" if ([TapResearch canShowContentForPlacement:placementTag]) { [TapResearch showContentForPlacement:placementTag delegate:self completion:^(NSError * _Nullable error) { if (error) { NSLog(@"Error on showContent: %ld, %@", (long)error.code, error.localizedDescription); } }]; } ``` ### Passing custom parameters Custom parameters allow you to receive additional information about the user when they complete a survey. This can be used to filter out users who have already completed a survey, or to target specific users. These params will be returned via server-to-server callbacks. There are a maximum of 5 custom parameters that can be passed per placement. The below code is a zoomed-in view of the code above. > Parameters can only be ascii characters, Unicode is not supported. Swift ```swift let customParameters = ["param1": 123, "param2": "abc"] as [String : Any] TapResearch.showContent(forPlacement: placementTag, delegate: self, customParameters: customParameters){ (error: NSError?) in if ((error) != nil) { print("Error \(String(describing: error))") } } ``` ```objectivec title="ViewController.m" [TapResearch showContentForPlacement:placementTag delegate:self customParameters:@{@"custom_param_1" : @"test text", @"custom_param_2" : @12} completion:^(NSError * _Nullable error) { ``` ### Granting Boosts You can grant a boost to a user with the `grantBoost` function using Boosts you have set up for your app. First implement the `TapResearchGrantBoostResponseDelegate` to receive the result of granting a boost: ```swift func onTapResearchGrantBoostResponse(_ response: TapResearchSDK.TRGrantBoostResponse) { if response.success { // Perhaps update display in response to a successful boost } else { if let error = response.error { // Handle the error } } } ``` Grant a boost: ```swift // Assuming self implements TapResearchGrantBoostResponseDelegate TapResearch.grantBoost("BOOST_TAG", delegate: self) { (error: NSError?) in // Handle any error } ``` First implement the `TapResearchGrantBoostResponseDelegate` to receive the result of granting a boost: ```swift - (void)onTapResearchGrantBoostResponse:(TRGrantBoostResponse * _Nonnull)response { if (response.success) { // Perhaps update display to indicate boost granted } else { if (response.error) { // Handel the error } } } ``` Grant a boost: ```swift // Assuming self implements TapResearchGrantBoostResponseDelegate [TapResearch grantBoost:@"YOUR_BOOST_TAG" delegate:self errorHandler:^(NSError * _Nullable error) { //Handle any error }]; ``` #### The `TRGrantBoostResponse` object: ```swift @objc public final class TRGrantBoostResponse : NSObject, Encodable { @objc public let boostTag : String @objc public let success : Bool @objc public let error : NSError? } ``` ## Notifications for some callbacks :::info From SDK v3.8.0--rc0 you can register for NSNotifications instead of implementing handlers for some callbacks. ::: To register for each of these you simply setup an observer for the notification(s), for example `TapResearchContentShown`: ``` func registerForNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(handleTapResearchNotification(_:)), name: TapResearch.TapResearchContentShown, object: nil) ... } @objc func handleTapResearchNotification(_ n: Notification) { // Check which notification then handle it ... } ``` Available notifications: * `TapResearch.TapResearchSDKReady` * `TapResearch.TapResearchDidError` * `TapResearch.TapResearchContentShown` * `TapResearch.TapResearchContentDismissed` Notification userInfo keys: * `TapResearch.ErrorKey` * `TapResearch.PlacementTagKey` ### onTapResearchSdkReady Callback This notification will be posted to the default NotificationCenter when the SDK is ready to be used. You can listen for `TapResearch.TapResearchSDKReady` instead of using a global `onTapResearchSdkReady` callback with TapResearchSDKDelegate. ### onTapResearchDidError Callback This notification will be posted to the default NotificationCenter with an error if one occurs. You can listen for `TapResearch.TapResearchDidError` instead of using a global `onTapResearchDidError` callback with TapResearchSDKDelegate. The NSError object is passed in the notification's userInfo for the key `TapResearch.ErrorKey`. ``` if let error = notification.userInfo?[TapResearch.ErrorKey] as? NSError { ... } ``` ### onTapResearchContentShown Callback This notification will be posted to the default NotificationCenter wihen TapResearch content is shown. You can listen for `TapResearch.TapResearchContentShown` instead of implementing a callback with TapResearchContentDelegate. The placementTag value is passed in the notification's userInfo for the key `TapResearch.PlacementTagKey`. ``` if let placementTag: String = notification.userInfo?[TapResearch.PlacementTagKey] as? String { ... } ``` ### onTapResearchContentDismissed Callback This notification will be posted to the default NotificationCenter wihen TapResearch content is shown. You can listen for `TapResearch.TapResearchContentShown` instead of implementing a callback with TapResearchContentDelegate. The placementTag value is passed in the notification's userInfo for the key `TapResearch.PlacementTagKey`. ``` if let placementTag: String = notification.userInfo?[TapResearch.PlacementTagKey] as? String { ... } ``` --- ## Need help? If you're experiencing issues or need assistance, reach out to [developers@tapresearch.com](mailto:developers@tapresearch.com). --- **Stay updated:** watch the [TapResearch iOS SDK on GitHub](https://github.com/TapResearch/TapResearch-iOS-SDK) to get notified about new releases. --- # iOS Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/survey-wall-preview/ios-surveys :::info Test Mode If your app is in test mode, you **must** use a test user. Configure test users in **"Test Devices"** on the dashboard. ::: ## Installation **See [here](../basic-integration/sdk-integration/ios) for TapResearch SDK for iOS integration documentation.** __Example apps are available [here](https://github.com/TapResearch/demo-apps/tree/ios-surveys-example/ios)__ ### Before You Begin * You must be on SDK v3.4.2+ to have access to Native Survey Tiles. * The wall preview feature needs to be enabled and set up by the TapResearch team so please reach out to your account manager when you are ready to get started * Native Survey Tiles should be on a dedicated placement, separate from the standard Survey Wall (if applicable) * **Always wait** until the SDK is ready (via the `onTapResearchSdkReady` callback or by checking `TapResearch.isReady()`) before calling any Native Survey Tiles interfaces * Implement the `TapResearchSurveysDelegate` delegate callback `onTapResearchSurveysRefreshedForPlacement` and set it before calling `getSurveys`, `showSurveyWithSurveyId` or `showSurvey` functions. :::info **Support** Any other questions? Reach out to your account manager or via Slack and we'll be happy to help! ::: ## Introduction ### Key Considerations & UI Design ## Integration ### SDK Interfaces #### Checking for available surveys When you have received notification that the SDK is ready (see [here](../basic-integration/sdk-integration/ios#checking-if-the-sdk-is-ready)) you should first check if there are surveys available for your placement: ```swift TapResearch.hasSurveys(for placementTag: <#String#>, errorHandler: <#((NSError?) -> Void)? = nil#>) -> Bool { } ``` ```objectivec BOOL hasSurveys = [TapResearch hasSurveysFor:<#(NSString * _Nonnull)#> errorHandler:<#^(NSError * _Nullable)errorHandler#>]; ``` #### Getting available surveys Get the surveys for your placement. Note: Attempting to fetch surveys before receiving the SDK Ready callback will result in an error and no surveys will be returned. ```swift TapResearch.getSurveys(for placementTag: <#String#>, errorHandler: <#((NSError?) -> Void)? = nil#>) -> [TRSurvey] { } ``` ```objectivec NSArray *surveys = [TapResearch getSurveysFor:<#(NSString * _Nonnull)#> errorHandler:<#^(NSError * _Nullable)errorHandler#>]; ``` #### Check that a survey can be shown Before showing, it is always best to check if the survey can be shown using `canShowSurvey`: ```swift TapResearch.canShowSurvey(surveyId: <#String#>, forPlacementTag placementTag: <#String#>) -> Bool { } ``` ```objectivec BOOL canShow = [TapResearch canShowSurveyWithSurveyId:<#(NSString * _Nonnull)#> forPlacementTag:<#(NSString * _Nonnull)#>]; ``` #### Showing a survey Showing a survey works similarly to `showContent` used for showing TapResearch survey content: after checking if a survey can be shown, call `showSurvey` with a survey ID, placement tag and a content delegate to receive `onTapResearchContentShown` and `onTapResearchContentDismissed` callbacks, for more information about these callbacks see [here](../basic-integration/sdk-integration/ios#tapresearchcontentdelegate). ```swift TapResearch.showSurvey(surveyId: <#String#>, forPlacementTag placementTag: <#String#>, delegate: <#TapResearchContentDelegate#>, errorHandler: <#((NSError?) -> Void)? = nil#>) { } ``` ```objectivec [TapResearch showSurveyWithSurveyId:<#(NSString * _Nonnull)#> placementTag:<#(NSString * _Nonnull)#> delegate:<#(id _Nonnull)#> errorHandler:<#^(NSError * _Nullable)errorHandler#>]; ``` If you need to pass custom parameters use: ```swift TapResearch.showSurvey(surveyId: <#String#>, forPlacementTag placementTag: <#String#>, delegate: <#TapResearchContentDelegate#>, customParameters: <#[AnyHashable:Any]#>, errorHandler: <#((NSError?) -> Void)? = nil#>) { } ``` ```objectivec [TapResearch showSurveyWithSurveyId:<#(NSString * _Nonnull)#> placementTag:<#(NSString * _Nonnull)#> delegate:<#(id _Nonnull)#> customParameters:<#(NSDictionary * _Nonnull)#> errorHandler:<#^(NSError * _Nullable)errorHandler#>]; ``` ### Callback Native Survey Tiles adds a **required** callback that lets you know when surveys have been refreshed. #### Setting the delegate **Required:** Set the delegate using `setSurveysDelegate` before calling `getSurveys`, `showSurveyWithSurveyId` or `showSurvey` functions. If the delegate is not set the SDK will send errors using the `TapResearchSDKDelegate` error callback `onTapResearchDidError` and Native Survey Tiles will not work. ```swift TapResearch.setSurveysDelegate(_ delegate: <#TapResearchSurveysDelegate?#>) { } ``` ```objectivec [TapResearch setSurveysDelegate:<#(id _Nullable)#>]; ``` To remove the delegate call the `setSurveysDelegate` with `nil`. #### Handling the callback You will see calls to the callback function when surveys have been completed, after any TapResearch content has been dismissed. Make sure the delegate object implements `TapResearchSurveysDelegate` and updates local storage and displays with the updated surveys: ```swift func onTapResearchSurveysRefreshed(forPlacement placementTag: String) { } ``` ```objectivec - (void)onTapResearchSurveysRefreshedForPlacement:(NSString *)placementTag { } ``` --- # iOS Orientation Handling Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/ios-alternate-orientation-handling # iOS orientation handling It's important that landscape apps not lock the screen rotation when rendering TapResearch. Landscape orientation within the TapResearch experience would result in degraded engagement and revenue performance. By contrast, apps that allow rotation to portrait mode for TapResearch screens typically achieve twice the earnings of apps that are locked in landscape. For the cross-platform guidance that applies to every SDK platform, see [Screen Orientation](./screen-orientation.md). ## Alternate orientation handling If you are in a situation where enabling portrait for iPhone is not possible in Xcode's project settings you can use this alternate method for allowing portrait for the TapResearch iOS SDK. **This assumes you are not using a SceneDelegate.** ## 1. Set allowed orientations. In the Xcode project editor's "General" tab set the required orientations in the "Deployment Info" secton. ## 2. Add an iPhone check helper function. We only need to make sure that portrait is available for iPhone, add this simple helper function to your AppDelegate: ```objectivec - (BOOL)isPhone { return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone); } ``` ```swift var isPhone: Bool { return UIDevice.current.userInterfaceIdiom == .phone } ``` ## 3. Add supported orientation function to AppDelegate ### 3.1. Add a boolean flag for allowing orientations Add a boolean property to AppDelegate: ```objectivec @property (nonatomic, assign) BOOL allowAllOrientations; ``` ```swift var allowAllOrientations: Bool = false ``` ### 3.2. Set boolean to NO/false If using Objective-C, inside `application:didFinishLaunchingWithOptions:` as soon as the function is entered set the boolean to NO. If using Swift make this part of the variable definition (as seen in 3.1 above). ### 3.3. Add supported orientations function to AppDelegate If you are using a SceneDelegate this function is ignored. ```objective-c - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { if ([self isPhone]) { if (self.allowAllOrientations) { // App supports all except upside-down return UIInterfaceOrientationMaskAllButUpsideDown; } else { // Initial launch screen supports only Landscape return UIInterfaceOrientationMaskLandscape; } } else { // iPad return UIInterfaceOrientationMaskAll; } } ``` ```swift func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { if isPhone { if allowAllOrientations { return [.allButUpsideDown] } else { return [.landscape] } } else { // iPad return [.all] } } ``` ### 3.4. Set boolean flag to YES When you are ready to allow portrait set the boolean to `YES` (or `true` in Swift). ## 4. LaunchScreen.storyboard If you are using a LaunchScreen.storyboard make sure it is a basic UIViewController without any outlet connections, no view controller subclass is needed for this. ## 5. Using SceneDelegate If you are using a SceneDelegate the AppDelegate's `application:supportedInterfaceOrientationsForWindow:` is ignored and you will need to return supported orientations use per view controller: ```objectivec - (UIInterfaceOrientationMask)supportedInterfaceOrientations { // For example: return UIInterfaceOrientationMaskLandscape; } ``` ```swift override var supportedInterfaceOrientations: UIInterfaceOrientationMask { // For example: return .landscape } ``` You can do this globally and just let your root view controller report available orientations. --- # Overview Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/integration-options # SDK overview The TapResearch SDK is the fastest way to add surveys to a native app. It handles survey retrieval, ranking, eligibility, profiling, and reward callbacks for you. :::tip Creating your app In the [Supplier Dashboard](https://www.tapresearch.com/supplier_dashboard/overview), create a new app and select **SDK**, then the platforms you'll ship on (iOS, Android, or both). See the [Setup checklist](../setup) for the full flow. ::: There are **two ways to present surveys** with the SDK — use either, or both together.
Survey wall (standard)

Add a call-to-action — a button, banner, or menu item. When the user taps it, TapResearch opens the full-screen survey wall in a web view and takes over. The quickest path: one entry point, and we handle the whole experience.

Native Survey Tiles

The SDK returns an array of the surveys a user qualifies for, so you render them as native tiles in your own UI — reward amount, estimated time, and all. The user taps a tile and that survey launches directly.

You can mix the two — for example, surface a few preview tiles in your feed and a CTA into the full wall elsewhere. :::info Platform guides [iOS](sdk-integration/ios) · [Android](sdk-integration/android) · [React Native](sdk-integration/react-native) · [Unity](sdk-integration/unity) ::: ## What the SDK handles for you - **Profiling & targeting** — users are matched to surveys by demographics and eligibility automatically. - **Quick Questions** — short, in-app questions you can ask your own users; only available through the SDK. See [Quick Questions](../3.2-only/quick-question-how-to). - **Survey ranking, eligibility filtering, and reward callbacks** — handled for you, so integration stays light. :::tip Want a fully custom, server-side experience? If you'd rather fetch surveys from your backend and render everything in your own UI without the SDK, use the [Survey Feed API](../survey-feed-api/overview) instead. ::: --- # Integrate with AI Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/integrate-with-ai # Integrate the SDK with AI Prefer to let an AI coding agent do the heavy lifting? Pick your platform and copy the prompt below into Claude, ChatGPT, Cursor, Codex — whatever you use. It points the agent at our full, always-current SDK reference for that platform, so it builds from the real documentation instead of guessing. Your agent can scaffold initialization, reward callbacks, and placement display from there. You stay in control — review what it writes, and dive into the rest of these docs whenever you want the details yourself. ## After you integrate: verify it's working Once the code is in place, run through these checks on a **test device**. **Your AI agent can help** — it can write a smoke test, walk you through each step, and debug failures. What it *can't* do is see your account, so your app's **Integration** tab in the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview) is what confirms each step actually passed — it detects each one from real activity and marks it done. :::tip Use a real test session These checks only register from a real test user reaching the SDK — firing a test postback on its own won't complete them. Add a test device first (**More → Test devices**); only test users can initialize the SDK before go-live. ::: 1. **SDK initialization received** — Initialize the SDK with your app's API token. The Integration tab marks this once it sees your first session. 2. **Enter survey wall from app** — Show a placement and reach the survey wall. (You'll need a placement with content configured on the **Manage** tab first.) 3. **Reward callback configured** — Set a server-to-server postback URL (this auto-completes), or implement the in-app reward handler. 4. **Generate test survey complete** — Complete a test survey so a completion is recorded against your app. 5. **Reward redeemed** — Confirm your callback or handler redeems the reward for the test user. If a step doesn't check off, it's the fastest signal of where the integration broke. For deeper testing guidance, see [Testing & QA](/docs/3.x/testing-and-qa). --- # Quick Question How-To Source: https://supply-docs.tapresearch.com/docs/3.x/3.2-only/quick-question-how-to Quick Questions allow you to ask up to **three questions** to your users at any point in your app. Currently available question types include: * Multiple choice * Open-ended text responses Follow the steps below to set up your first Quick Question: ## Step 1: Create a Currency If you haven't already, [create a currency](../setup#5-add-a-currency) for rewarding users. ## Step 2: Create a Placement Ensure you have a placement created for where the Quick Question will appear. A placement defines the physical location in your app (e.g., home screen, earn center). **Important:** Quick Questions must be attached to an **"Automatic" Placement**, not a "Manual" one. Unlike survey walls, Quick Questions are triggered automatically to users matching your targeting criteria—they don't require a button click. ## Step 3: Create Your Quick Question Content Once your placement is ready: 1. Click the **Create content** button in the upper right corner 2. Select **"Quick Questions"**”
## Step 4: Build Your Questions In the content builder: 1. Choose your question type (multiple choice or open-ended) 2. Fill in the required fields 3. Add up to **3 questions** per Quick Question content
### Question Guidelines **Multiple Choice Questions:** * No character limit on questions or answers, but check the preview to ensure readability * No limit on answer options, but consider user experience (more options = more scrolling) * **Best practice:** Keep answer options to 7 or fewer for optimal data quality * Use the randomize toggle to avoid order bias and maintain data integrity **Open-Ended Questions:** * Answers are limited to **250 characters**
## Step 5: Configure Display Settings Once your questions are composed: 1. Navigate to the **"Configure"** tab 2. Choose the platform(s) (iOS, Android, etc.) 3. Select the placement where the Quick Question will be shown ## Step 6: Set Up Rewards Determine whether to reward users for answering the Quick Question, and if so, specify the reward amount. ## Step 7: Add Survey Wall Link (Optional) Choose whether to include a link to the TapResearch survey wall at the end of the Quick Question. This allows users to continue to more survey opportunities. **Note:** * You can select any currency that has an associated survey wall in your app * The currency can differ from the one used for Quick Question rewards ## Step 8: Configure Response Settings Set your response limits: * Cap the number of participants * Set an end date/time for the Quick Question ## Step 9: Define Target Audience Specify which users will see this Quick Question based on their attributes. **Learn more:** For detailed information about user attributes and targeting, see our [User Attributes and Targeting documentation](../operational-tools/user-attributes). ## Step 10: Launch Your Quick Question Once all steps are complete: 1. Click **"Launch Quick Question"** in the upper right corner 2. Your Quick Question will go live and responses will start coming in! --- **Need help?** Contact developers@tapresearch.com or your Account Manager for additional support. --- # Handling Quick Question Responses Source: https://supply-docs.tapresearch.com/docs/3.x/3.2-only/handling-quick-question-responses ## Overview There are several ways to ingest Quick Question responses: * **In-app callback** - Real-time responses within your app * **Server-to-server postback** - Automated server notifications * **CSV download** - Manual export from the dashboard * **API download** - Programmatic data retrieval Select an approach based on how you plan to use the Quick Question response data. ## In-App Callback In-app callbacks are fired whenever a user completes a Quick Question. **Use this method when you need real-time access to response data.** Quick Question callbacks are configured during SDK initialization. See the platform-specific documentation below for implementation details: * [iOS Quick Question Response](../basic-integration/sdk-integration/ios#quick-question-response) * [Android Quick Question Response](../basic-integration/sdk-integration/android#quick-question-callback-setup-additional-information) * [Unity Quick Question Response](../basic-integration/sdk-integration/unity#quick-question-response-callback) * [React Native Quick Question Response](../basic-integration/sdk-integration/react-native#quick-question-response) ## Server-to-Server Postback Server-to-server postbacks are fired after a user answers a Quick Question, if a postback URL is configured in your app settings. **Note:** Setting up a postback URL does not prevent in-app callbacks from firing—both can be used simultaneously. ### Setup Instructions 1. Navigate to your app title and click **Settings** 2. Select the platform (iOS and Android are configured separately) 3. Add your Quick Question postback endpoint to **"Quick Questions data postback URL"** 4. Click **"Test data postback"** to verify the endpoint 5. Click **"Save Changes"** **Important:** You must receive a 200 response from the test postback to save the URL. ### Example Payload ```json { "type": "quick_questions", "payload": { "survey_identifier": "YOUR_SURVEY_IDENTIFIER", "app_name": "YOUR_APP_NAME", "api_token": "YOUR_API_TOKEN", "sdk_version": "3.5.3", "platform": "Android", "placement_tag": "YOUR_PLACEMENT_TAG", "user_identifier": "YOUR_USER_IDENTIFIER", "user_locale": "en", "seen_at": "2025-10-13T22:31:12.455Z", "complete": { "complete_identifier": "tap_qq_68_fb67345f15e08892a0635b7ddcac3138", "completed_at": "2025-10-13T22:31:24.869Z" }, "questions": [ { "question_identifier": "50e2ed203cd2b810ba982b7ce9841939", "question_text": "This is the question", "question_type": "multiple_choice__single", "user_answer": { "value": "Answer F", "identifiers": [ "78bf19eaad5a8bef5c804db7f8760f58" ] } }, { "question_identifier": "e2b3f38f2c5f423ed8303bd68dc2dc77", "question_text": "Test", "question_type": "open_text", "user_answer": { "identifiers": [], "value": "My answer" } } ], "target_audience": [ { "filter_attribute_name": "some_attribute_name", "filter_operator": "equals", "filter_value": "SomeValue", "user_value": "SomeValue" } ] } } ``` ## CSV Download CSV download is available for any Quick Question through the publisher dashboard. **To download:** 1. Click the **"..."** dropdown in the top right corner of your Quick Question content card 2. Select **"Download CSV"** The CSV file contains all current responses and is updated in real-time. ## API Download API download is available for all Quick Questions. **This method is ideal when:** * Quick Question data is not needed immediately * You want to pull all response data on a scheduled basis * You're ingesting data into your own data warehouse **Note:** This data is updated on a daily basis. ### Getting Started See the [Reporting API Documentation](../operational-tools/reporting-api.md#quick-question-results) for implementation details. To get access to the stats API, contact our account management team at [developers@tapresearch.com](mailto:developers@tapresearch.com). --- # Screen Orientation Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/screen-orientation ## Screen Orientation **Critical for landscape apps:** Do not lock screen rotation when displaying TapResearch content. ### Why This Matters * **Landscape-locked apps** experience degraded engagement and revenue performance * **Apps allowing portrait rotation** for TapResearch typically achieve **2x the earnings** of landscape-locked apps ### Implementation Allow your app to rotate to portrait mode when displaying TapResearch screens for optimal performance. **Unity developers:** See the [screen orientation guide](unity#screen-orientation-for-landscape-applications) for platform-specific instructions. --- # Callbacks Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/callbacks ## Overview Callbacks (also called postbacks) allow TapResearch to notify your web service when: * Users receive rewards * The SDK successfully initializes (in-app only) **TapResearch supports two callback types** (choose one per app): * **In-app callbacks** - Default method * **Server-to-server callbacks** - Requires custom configuration ## In-App Callbacks In-app callbacks report rewards as a collection in a single RewardCollection callback. **Example:** If a user completes three surveys, the SDK fires one callback containing all three rewards. **Key features:** * TapResearch notifies users of their rewards * Default callback method (fires unless server-to-server is configured) **When callbacks fire:** * When the user closes TapResearch * After successful SDK initialization ## Server-to-Server Callbacks Server-to-server callbacks are fired immediately after a user earns a reward. **Important:** Your app must notify users of rewards received (TapResearch does not notify users with this method). :::info By default, server-to-server callbacks use **GET requests**. ::: ## Update Callback URL Update your callback URL in the **"Edit App"** section of the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview). ## URL Validation **Requirements:** * No spaces or special characters * Must return a 2xx response code to be considered valid ## Callback Testing Use the **Test Callback** button to generate a test completion that fires a callback to your specified URL. **Best practice:** Test callbacks before deploying to production. :::note Test callbacks use fixed currency values. Commercial survey values will vary. ::: ## Sample Callback Requests ### GET Request ``` https://tapresearch.com/postback?api_token=your-server-api-token&cpid= tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout _amount=191&payout_currency=gold&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid =developers%40tapresearch.com&sig=b438afd7426c743e777c387d7e2712d4 ``` ### POST Request **Note:** Contact developers@tapresearch.com to enable POST requests. **URL:** ``` https://tapresearch.com/postback?api_token=your-server-api-token ``` JSON Body ```json { "uid": "developers", "did": "0258DE71-DFEB-4399-BF26-11C1F6E600D2", "tid": "777ca23551a4a9173920c22e1ed7f4f3", "cpid": "tap_37939e4ede350f3a8d5149d2fcaa025e", "api_token": "the api token found in your settings", "payout_amount": 191, "payout_currency": "gold", "revenue": 0.5, "payout_type": 3, "ip_address": "10.0.0.1", "sig": "b438afd7426c743e777c387d7e2712d4" } ``` ## Payload |Parameter|Type|Description| |---------|----|-----------| |uid|String|Persistent unique identifier for this user. | |did|String|Device identifier -- IDFA for Apple, Google Device ID for Android (May not be present if the user opted out of ad tracking).| |tid|String|Transaction ID or click ID. This value will not be unique if the user completed multiple surveys in a single session. Use cpid for deduping purposes.| |cpid|String|Unique survey complete identifier. We recommend that you store and check against this value to ensure you reward the user only once per survey complete.| |api_token|String|This is the unique identifier we generate when you create an app in our dashboard.| |payout_amount|Integer|Amount of currency earned by this user.| |payout_currency|String|The type of Currency the user earned.| |revenue|Decimal|Amount you will be paid for this survey complete in USD.| |payout_type|Integer|The action that the user was rewarded for. 0 - Profile Complete, 3 - Survey Rewarded, 9 - Quick Question Completed.| |ip_address|String|This is the IP address of the respondent when the transaction occurred.| |sig|String|This is the URL signature, which is an HMAC-MD5 generated using the query string, minus the sig parameter. See the security section for more details.| :::note The parameters above aren't the complete set The callback may include additional parameters beyond those listed here, and more may be added over time. This is expected — the signature always covers the **entire** query string, so validate against whatever you actually receive rather than a fixed list of fields (see [Security](#security)). ::: ## Security For security purposes, every callback request will include an HMAC-MD5, which is generated using the query string. **Sign the entire query string exactly as you receive it, minus `sig` — don't rebuild it from a fixed list of parameters.** The signature covers every parameter present, so hashing whatever arrives (as the steps and code below do) stays correct even as parameters are added. Using the example request in the callback section, here are the steps to generate the HMAC-MD5. Grab your API secret. This value is located in your main dashboard. Example: 26dcc0fc7b6208fdfeffaf19f627cb4a * A callback request will look similar to the example shown below: https://tapresearch.com/postback?api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers%40tapresearch.com&sig=b438afd7426c743e777c387d7e2712d4 * Isolate the query string, leaving out the question mark. Example: `api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers%40tapresearch.com&sig=b438afd7426c743e777c387d7e2712d4` * Strip out the `sig` parameter, including the ampersand (&) symbol. Example: `api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers%40tapresearch.com` * Decode the query string. Example: `api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers@tapresearch.com` * Run the API secret and the modified payload through your favorite HMAC-MD5 generator. Using the example values in steps 1 and 2, the resulting string will be `b438afd7426c743e777c387d7e2712d4`. * Check your generated HMAC against the `sig` param value. If they match, then record a complete and reward the user. ### Whitelisted IP Addresses To enhance security, you can restrict incoming callback requests to the following TapResearch IP addresses: | IP Address | |------------| | 34.198.225.203 | | 3.88.121.21 | | 3.89.214.250 | | 3.92.129.242 | ```ruby # Sample request URL url = "https://tapresearch.com/postback?api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers%40tapresearch.com&sig=b438afd7426c743e777c387d7e2712d4" # Isolate query string query_string = url.gsub(/^(.*?)\?/, "") # Strip out the sig parameter stripped_query_string = query_string.gsub(/&sig.*/, "") # Decode URL decoded = URI.decode(stripped_query_string) # Generate HMAC-MD5 api_secret = "26dcc0fc7b6208fdfeffaf19f627cb4a" digest = OpenSSL::Digest.new("md5") md5 = OpenSSL::HMAC.hexdigest(digest, api_secret, decoded) puts md5 # b438afd7426c743e777c387d7e2712d4 ``` ```php ``` ```java private static final String API_SECRET = "26dcc0fc7b6208fdfeffaf19f627cb4a"; private static final String ALGO = "HmacMD5"; try { // URL String urlString = "https://tapresearch.com/postback?api_token=9b99ccc0062035544a5b6579b0cfc954&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&did=0258DE71-DFEB-4399-BF26-11C1F6E600D2&payout_amount=191&revenue=0.5&tid=777ca23551a4a9173920c22e1ed7f4f3&uid=developers%40tapresearch.com&sig=b438afd7426c743e777c387d7e2712d4"; // Generate HAMC-MD5 Mac mac = Mac.getInstance(ALGO); SecretKeySpec key = new SecretKeySpec(API_SECRET.getBytes(StandardCharsets.UTF_8), ALGO); mac.init(key); // Isolate query string and remove sig parameter String queryString = urlString.substring(urlString.indexOf("?") + 1); queryString = queryString.substring(0, queryString.indexOf("&sig=")) + queryString.substring(queryString.indexOf("&sig=") + 25); // Decode URL String decode = URLDecoder.decode(queryString, StandardCharsets.UTF_8); byte[] bytes = mac.doFinal(decode.getBytes(StandardCharsets.UTF_8)); String md5 = DatatypeConverter.printHexBinary(bytes).toLowerCase(); System.out.println(md5); // b438afd7426c743e777c387d7e2712d4 } catch (Exception e) { e.printStackTrace(); } ``` ## Retries If a callback fails, our system will continue retrying to send the reward for 48 hours OR until a 2xx response code is returned. ## Callback Visualization --- # TRReward Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/tr-reward # TRReward The reward object the SDK returns when a user earns a reward. It is the same across all platforms (iOS, Android, React Native, and Unity). ## Properties * **`transactionIdentifier`** (String) * Unique identifier for the transaction * Use this for deduplicating reward callbacks in your app or server-to-server implementation * **`placementTag`** (String) * The placement tag where the reward was earned * **`placementIdentifier`** (String) * The placement identifier where the reward was earned * **`currencyName`** (String) * Name of the rewarded currency * **Note:** This is the "Currency Name" from the dashboard, not the "Display Name" * **`payoutEventType`** (String) * Type of payout event. Possible values: * **`profile_reward`** — User completed the profiling flow * **`partial_payout`** — User started a survey but was disqualified * **`full_payout`** — User successfully completed a survey * **`rewardAmount`** (Int) * Amount of currency rewarded (in your app's currency) --- # SDK Error Codes Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/sdk-error-codes # SDK Error Codes When using the SDK, you may encounter the following error codes: | Code | Description | |-------|---------------------------------------------------------------------------------| | 1000 | Invalid API token | | 1001 | Missing user identifier | | 1002 | Service unavailable | | 1003 | Missing header | | 1004 | Network timeout | | 1005 | Unknown network error | | 1006 | Javascript execution error | | 1007 | App unavailable | | 1008 | No internet connection | | 2000 | Missing required tag or invalid placement tag | | 2001 | No content available for this placement | | 3000 | Missing attribute | | 3001 | No configuration for landscape orientation while attempting to show in landscape| | 3002 | No configuration for portrait orientation while attempting to show in portrait | | 4000 | Invalid reward identifier | | 6000 | Not initialized | | 6001 | Missing API token | | 6002 | Unable to present UI | | 6003 | Unable to create UI | | 6004 | Unable to decode TRError JSON | | 6005 | Reward delegate was deallocated or not set | | 6006 | Bad or missing custom parameter | | 6007 | Missing application context | | 6008 | Missing placement tag | | 6009 | Missing content callback | | 6010 | SDK not ready | | 6011 | Unable to convert user attributes | | 6012 | Invalid survey Id for placement | | 6013 | No surveys for placement | | 6014 | Quick Questions data delegate was deallocated | | 6015 | Invalid boost tag | | 6016 | Too many requests | | 6017 | Placement details missing | | 10000 | Invalid notification command | | 10001 | Notification missing placementTag while command is showContent | | 10002 | Notification missing scheduledDate and timeDelta, cannot schedule | ## Debugging To assist with debugging, please provide: * The error code you're encountering * A complete stack trace of the error * Any relevant context about when the error occurs Contact developers@tapresearch.com with this information for support. --- # Initialization Options Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/initialization-options The SDK supports a `TapInitOptions` object for additional configuration. The structure is shown below (Kotlin example): ```kotlin data class TapInitOptions( val userAttributes: HashMap? = null, val clearPreviousAttributes: Boolean? = false, ) ``` ## Configuration Options | Key | Description | Required? | |-----------------------------------------|------------------------------------------------------------------------------------------------|-----------| | `userAttributes` | User attributes hash, see usages [here](./user-attributes) | No | | `clearPreviousAttributes` | Will clear our any attributes stored on tapresearch backend for the current user | No | ## User Attributes in TapInitOptions **Best practice:** If user attributes are known at SDK initialization, pass them through `initOptions` instead of using `sendUserAttributes`. **Benefit:** Faster load times for targeted content. ## Clearing Previous Attributes User attributes sent from the SDK (via `TapInitOptions` or `sendUserAttributes`) are stored on the TapResearch backend for targeting purposes. **Use `clearPreviousAttributes`** to remove all previously stored attribute values for a user. This is useful when: * A user logs out * User data needs to be reset * Starting fresh with new attributes --- # User Attributes and Targeting Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/user-attributes User attributes allow you to store additional information about users as key-value pairs. Use them to track information such as: * Game level * Payer status * Custom user segments * Any other data useful to your organization ## SDK Setup This page covers how to set up user attributes in the dashboard. **Important:** Attributes only appear in the dashboard filters **after they've been sent from the SDK at least once**. For platform-specific implementation details, see: - [Sending User Attributes -- Android](../basic-integration/sdk-integration/android#send-user-attributes) - [Sending User Attributes -- iOS](../basic-integration/sdk-integration/ios#send-user-attributes) - [Sending User Attributes -- Unity](../basic-integration/sdk-integration/unity#send-user-attributes) - [Sending User Attributes -- React Native](../basic-integration/sdk-integration/react-native#send-user-attributes) ## Targeting Based on User Attributes You can add up to **3 filters** per piece of content. **Note:** Capping and pacing is only available for currency sale banner content, not Multiple Choice questions. ### Example: Targeting Users in France The screenshot shows a partial list of targeting criteria received by TapResearch. Sending different attributes enables you to target different user segments. ### Targeting Operators **String values:** * `is equal to` - Match a specific value or list of values * `contains` - Match any value in a list **Numerical values:** * `is equal to` * `is greater/equal to` * `is less/equal to` **Multiple values for one attribute:** Add each value as its own separate entry in the filter — for example, three separate entries for `US`, `CA`, and `GB` when targeting multiple countries, or three separate entries for each tenure level. **Do not** enter them as a single comma-separated string (e.g. `US, CA, GB`) — the filter will treat that as one literal value and it won't match anything. ## Important Limitations * **Case sensitivity** - Attributes are case-sensitive: `test` and `Test` are different attributes * **Dates** - Stored as strings: `2020-01-01` and `2020-1-1` are different values * **Booleans** - **Do not use native booleans**. Use stringified values (`"true"`) or integers (`0`, `1`) instead * **Integer vs String** - `1` and `"1"` are different values. For numerical comparisons, send integer values ## Built-in Attributes The SDK automatically sends these built-in attributes: ### Country The user's country location. * Use 2-letter ISO country codes (e.g., `US` for United States) ### First Seen At The first time TapResearch saw this user. * Use ISO8601 date strings: `2020-01-01T00:00:00.000Z` * Use relative dates: `3d` targets users seen more than 3 days ago * Date-only format works: `2020-01-01` equals `2020-01-01T00:00:00.000Z` ### Test Devices Targets only devices in your Test device list. * Use for testing content before going live ### Audience Percentage Targets a percentage of users. * Select groups out of 10 (each group = 10% of users) * Example: Select 3 groups to target 30% of users ## Examples ```kotlin userAttributes = hashMapOf( "testStatus" to "VIP", "testInt" to 2, "testIntString" to "2", "vip_status" to "super-vip", "Some Date" to Instant.now().toString(), // 2023-08-31T10:15:30.00Z // The below will match on relative dates, so you can filter by "a Month Ago" >= 3d "a Month Ago" to LocalDate.now().minus(1, ChronoUnit.MONTHS).toString(), // 2023-07-31 "testCAPSUpper" to "CAPS", // Matches "CAPS" but not "caps" "testCAPSLower" to "caps", // Matches "caps" but not "CAPS" ), ``` ## Technical Details ### Attribute Caching User attributes are cached for **1 minute** on the backend. * Changes to attributes may take up to **1 minute** to be reflected when querying for content ### Caching Examples **Example 1: User Levels Up** Scenario: User sees a placement, levels up, and views the same placement again. ``` playerLevel = 1 changes to playerLevel = 2 ``` You have two pieces of content, one for level 1 and one for level 2. * The user shows content for a placement * The user's level is 1 * The user plays the game and levels up to 2 * The user clicks the placement again and its been less than 1m * The user sees the same content for that placement, not the level 2 content **Example 2: New Attribute Added** Scenario: You have two pieces of content, one for attribute 1 and one for attribute 2. ``` attribute1 = 1 ``` You add an attribute ``` attribute2 = 2 ``` The user has seen a placement, gets an additional attribute they did not have before, and sees new content for that placement * The user shows content for a placement * The user has an additional attribute added to their user attributes that did not exist before * The user clicks the placement again and its been less than 1m * The user sees the _new_ content for that placement, based on the filters for that content **Example 3: Date Targeting** * Relative dates * You want to show content to users who have been playing for 1 month * You'll want to target where `First seen at <= 30d` * Absolute dates * You want to show content to users who have been playing for 1 month * You'll want to target where `First seen at <= 2020-01-01T00:00:00.000Z` **Example 4: Boolean Values** * A user is a VIP * String * Send the attribute `is_vip = "true"` * You'll want to target where `is_vip = "true"` * Integer * Send the attribute `is_vip = 1` * You'll want to target where `is_vip = 1` **Example 5: Multiple Values** * You have multiple user types and want to target some of them * Send the attribute `user_type = "vip"` or `user_type = "normal"` or `user_type = "super-vip"` * Target where `user_type = "vip"` or `user_type = "super-vip"` using the `contains` operator — add `vip` and `super-vip` as **two separate entries** in the filter UI, not as one comma-separated value ## Troubleshooting ### Content Not Matching? **Common issue:** Sending strings instead of integers * Sending `"1"` (string) will not match filters expecting `1` (integer) * **Solution:** Send the value as an integer: `1` ---