# TapResearch Publisher Documentation — Full documentation # Source: https://supply-docs.tapresearch.com · generated at build time, do not edit by hand. # Each section below is one documentation page. # Welcome Source: https://supply-docs.tapresearch.com/docs/3.x/intro # Welcome to TapResearch Turn your engaged users into revenue. TapResearch rewards your users with your in-app currency for completing **surveys** — and other activities like **offers** — while we handle survey supply, matching, and reward reconciliation. ## Why TapResearch - **High-paying surveys from major research firms** — legitimate market-research surveys, matched to each user for relevance. - **Guaranteed payout** — users earn for their time even when a survey disqualifies them or fills up, so they keep coming back. - **Surveys + offers** — rewarded surveys at the core, complemented by an offerwall of app installs and other offers. - **Integrate your way** — from a low-code hosted link to a full native SDK. ## How it works 1. A user opens a TapResearch survey experience in your app. 2. They share some basic demographic info to build their profile. 3. We match them to surveys they qualify for. 4. They complete a survey, we notify your backend, and the user earns your in-app currency. ## Start here
Explore what you can offer

Surveys, the Offerwall, Bonus Bar, Currency Sales — the experiences you can put in front of your users.

See features →
Set up your account

Create your account and app, add a currency, and find your way around the dashboard. The same first steps for every integration.

Get set up →
Choose your integration

Compare the four ways to integrate — from a low-code hosted link to a full native SDK — and pick the path that fits your app.

Compare paths →
--- Questions along the way? Email [developers@tapresearch.com](mailto:developers@tapresearch.com) or open your [Publisher Dashboard](https://www.tapresearch.com/supplier_dashboard/overview). --- # Features Source: https://supply-docs.tapresearch.com/docs/3.x/features/overview # Features TapResearch is built around the **Survey Wall** — rewarded surveys your users complete to earn your in-app currency. A set of complementary features then lets you add more ways to earn, drive repeat engagement, and tune monetization. ## Core: Survey Wall
Survey Wall

A full-screen wall of thousands of high-paying surveys, matched to each user and fully customizable to your app. The heart of TapResearch — every integration delivers it.

Learn more → ## Build on it
Bonus Bar

An optional, gamified completion streak that rewards users as they finish surveys, then resets each period.

Learn more →
Currency Sales

Run time-limited boosted exchange rates — a "2× coins" event — across surveys and offers to spike engagement and completions.

Learn more →
Offerwall

A secondary surface of non-survey offers — app installs and engagement offers — so users can earn through activities beyond surveys.

Learn more →
User Boosts

Temporary reward multipliers for targeted user cohorts — first-timers, birthdays, win-backs, or your own custom audiences.

Learn more →
Standalone Profiling

Run the profiler on its own, decoupled from the survey wall — collect profile data up front for sharper targeting.

Learn more →
## SDK content types Extra in-app survey formats available with the [SDK](/docs/3.x/basic-integration/integration-options) integration:
SDK only
Native Survey Tiles

Render native survey tiles directly in your own UI, instead of opening the full wall.

Learn more →
SDK only
Quick Questions

Short, single-question prompts you can ask your own users anywhere in your app.

Learn more →
You can also place **banners** and **interstitials** as SDK content types — see [Placements](/docs/3.x/operational-tools/placements). --- # Survey Wall Source: https://supply-docs.tapresearch.com/docs/3.x/features/survey-wall # Survey Wall The Survey Wall is the **core of TapResearch** — a full-screen wall of high-paying surveys from major research firms, matched to each user, where they earn your in-app currency by completing surveys. Every integration delivers it. ## Why publishers choose it - **High-paying, high-converting surveys.** Users earn well and succeed more often — a bigger share of starts turn into completed, rewarded surveys. - **Consistently high fill.** A deep inventory from major research firms keeps the wall stocked, so users rarely run out of ways to earn. - **Built-in profiling.** TapResearch profiles each user and matches them to surveys they actually qualify for — more relevant surveys, fewer screen-outs, higher fill for you. - **Guaranteed payout.** Users are rewarded for their time even when they don't finish a survey — if they're disqualified or a survey is already full, they still earn a partial reward based on the time they spent. That keeps users engaged instead of hitting a dead end. ## The experience - A full-screen wall of available surveys, matched to each user. - Fully customizable to match your app's look and feel. ## Customization Make the survey wall feel native to your app — configure these in **App Settings** in the TapResearch dashboard: - **Header** — your branding at the top of the wall. - **Background color** and **button colors** (primary and secondary actions), set with HEX codes. - **Currency name and icon** — your reward, shown your way throughout the wall. ## Targeting SDK only Show the right surveys to the right users by targeting specific cohorts with [User attributes](../operational-tools/user-attributes). Targeting is available with the **SDK** integration. ## Prefer native tiles? If you'd rather render surveys as tiles inside your own UI instead of opening the full wall, see [Native Survey Tiles](native-survey-tiles). --- # Bonus Bar Source: https://supply-docs.tapresearch.com/docs/3.x/features/bonus-bar # Bonus Bar The **Bonus Bar** is a recurring, gamified reward that drives repeat engagement. Players fill a progress bar by completing surveys and unlock rewards as they reach **milestones**. The bar resets on a schedule (for example, weekly), so it's a repeatable loop rather than a one-time bonus. ## How it works - A player sees a progress bar with one or more **milestones**, each with a completion **threshold** and a **reward**. - Each qualifying survey completion advances the player's progress within the current **reset window**. - When their completion count reaches a milestone's threshold, the player earns that milestone's reward in your in-app currency. - At the end of the window the bar **resets**, and the player can earn the rewards again in the next window. Bonus Bar rewards are delivered just like any other reward in your integration — through your normal server-to-server reward callback — so there's nothing extra to build. ## Setting it up Configure the Bonus Bar in the **Supplier Dashboard → App Settings → Bonus bar** tab. Create one or more **configs**; each config defines a bonus bar for a set of players. ### Config settings | Setting | Description | | ------- | ----------- | | **Currency** | The in-app currency milestone rewards are paid in. Required — pick (or create) a currency before saving. | | **Schedule** | **Weekly (recurring)** — the bar resets every week on a day and time you choose. **One-time event** — the bar runs once between a start and end date. | | **Reset day & time** *(weekly)* | The day of the week and time the bar resets each week. | | **Start & end** *(one-time)* | The window during which a one-time bar is active. | | **Audience** | Apply the config to **all players**, or target a specific player **cohort**. You can have one config per cohort plus a default for everyone else. | ### Milestones Each config has an ordered list of **milestones**. For each milestone you set: - **Completes** — the number of qualifying completions required to unlock it. Must be a positive whole number, and each milestone's threshold must be unique within the config. - **Reward** — the amount of your in-app currency the player earns when they reach the threshold. Must be a positive whole number. Add as many milestones as you like. For example: | Completes | Reward | | --------- | ------ | | 3 | 100 | | 6 | 250 | | 10 | 600 | :::note Multiplier rewards A milestone reward doesn't have to be flat currency — it can also unlock a currency-sale-style **multiplier** (for example, 2× for a set period). TapResearch sets this up on the backend, so reach out if you'd like a milestone to reward a multiplier instead of (or alongside) a currency amount. ::: ## Things to know - **A currency is required.** Each config pays out in one of your app's currencies. - **One config per audience.** Target different cohorts with different bonus bars, plus a default config for everyone else. - **What counts as a "complete."** Only qualifying survey completions during the current window advance a player's progress. - **Reward amounts are whole numbers.** Milestone rewards are paid as whole units of your currency. - **Availability.** The Bonus Bar is available across all four integration paths. --- # User Boosts Source: https://supply-docs.tapresearch.com/docs/3.x/features/user-boosts # User Boosts **Boosts** temporarily multiply the rewards your users earn — a "2× coins" window for a specific group of users — to spark engagement at the moments that matter most: a user's first session, their birthday, or a win-back after they've gone quiet. :::info Enabled by TapResearch Boosts are set up and managed by the TapResearch team. **Reach out to your account manager** to turn them on for your app. ::: ## How it works You and your account manager pick **who** gets a boost, **how big** it is, and **how long** it lasts. While a user's boost is active, every survey they complete pays out at the boosted rate — `reward × multiplier` — and the boost appears in-app under a name you choose (for example, "Welcome Bonus" or "Birthday Bonus"). ## Built-in boosts | Boost | Who it targets | Typical setup | |-------|----------------|---------------| | **First-time user** | Users in their first session | 2× for 48 hours | | **Birthday** | Users on their birthday | 2× for 24 hours | | **Churned / inactive** | Users who've lapsed (e.g. no activity for 30+ days) | 2× for 48 hours, on a trigger you define | Each has a configurable multiplier (1.5×, 2×, 3×…), display name, and time window. ## Custom boosts Beyond the built-ins, you can run **custom boosts** for anything — a seasonal promo, a launch event, a holiday weekend. Same controls: pick the audience, the multiplier, and the dates. ## Target the right users The real power is **cohort targeting** — boosts go only to the user segments you care about, defined by: - **Attributes you collect** and send to TapResearch — age, location, tenure, spend tier, or anything you pass via [user attributes](../operational-tools/user-attributes) or custom targeting. - **TapResearch signals** like survey history or how long since a user first appeared. Cohorts can combine filters — for example, *"users who installed 14+ days ago and have never completed a survey"* — so a boost lands exactly where it'll move the needle. ## Why publishers use them - **Activate new users** — a first-session boost drives that all-important first complete. - **Win back churned users** — a boost is a strong reason to come back. - **Reward your best users** — give high-value cohorts a bigger multiplier. ## Related - A boost can also be the reward at a [Bonus Bar](bonus-bar) milestone (for example, *"complete 5 surveys → unlock a 1.5× boost"*). - When a user receives a boost, TapResearch can notify your backend so you can surface it in your own UI — see [Boost postbacks](../operational-tools/boost-postbacks). ## Turn it on Boosts are enabled per app by the TapResearch team. **[Reach out to your account manager](mailto:developers@tapresearch.com)** to get started. --- # Currency Sales Source: https://supply-docs.tapresearch.com/docs/3.x/features/currency-sales ## Overview Currency sales are **optional but highly recommended** as they drive user engagement and increase revenue. ### Content Types Supporting Currency Sales * **Survey wall** - Direct sale messaging in the survey wall * **Banner** - Display sale promotions as banners * **Interstitial** - Full-screen sale announcements Choose the content type that best fits your app's user experience. ## Promoting currency sales Let users know a sale is live with **your own messaging system**, or let TapResearch surface it for you using the **Banner** or **Interstitial** content types above. ## Configuration All content types supporting currency sales include these fields: * **Multiplier** - The multiplier applied to reward amounts * Example: Multiplier of 2 means 100 coins becomes 200 coins * **Starts on** - Sale start date and time * **Ends on** - Sale end date and time * **Interval** - How often the sale repeats * The **day** dropdown is based on the day of the week the sale starts ## Best Practices **Recommended schedule:** * Run sales every **1-2 weeks** * Duration: **2-4 days** per sale * Run sales on **weekdays**, when survey inventory is strongest * Run less frequent **Flash Sales** with a bigger multiplier every **2-3 months** for an extra engagement boost This frequency keeps users engaged without devaluing your currency. --- # Offerwall Source: https://supply-docs.tapresearch.com/docs/3.x/features/offerwall # Offerwall The **Offerwall** is a wall of **non-survey offers** — primarily mobile **app installs** and multi-step engagement offers (for example, "install this game and reach level 5"). It gives you a monetization surface beyond surveys: players earn your in-app currency by completing offer **goals**. :::info Offerwall vs. Survey Wall The Offerwall is a separate product from the [Survey Wall](./survey-wall). The Survey Wall rewards users for completing **surveys**; the Offerwall rewards them for completing **app-install and engagement offers**. ::: ## The player experience - Offers appear in an **"Offers" tab**, alongside the survey experience where the Offerwall is enabled. - Each offer card shows its title, image, a short description, and the reward. - A player opens an offer's **details**, taps it, and is taken to the app store or offer destination. - Many offers have **multiple goals** — for example, install the app (the main goal), then reach a milestone like a level or a number of days played (bonus goals). Each goal pays out its own reward. - Players can see their **in-progress and completed** offers. ## How rewards work When a player completes an offer goal, they earn your in-app currency for that goal. Because a single offer can have several goals, one offer can reward a player multiple times as they progress (install → reach level 5 → play for 7 days). Offerwall rewards are delivered just like any other reward in your integration — through your normal server-to-server reward callback — so there's nothing extra to build. ## Availability The Offerwall is **provisioned by TapResearch** for your app rather than configured in the dashboard. If you'd like to add it to your integration, contact your TapResearch account manager. A few things to know: - Offers are **platform-specific** (iOS or Android) and the available offers can vary by **country**. - The offer catalog is curated and managed by TapResearch. --- # Standalone Profiling Source: https://supply-docs.tapresearch.com/docs/3.x/features/standalone-profiling # Standalone Profiling By default, TapResearch profiles each user **inside** the survey wall — they answer a few profile questions on their way into surveys. With **standalone profiling**, you can run that profiling step on its own, **decoupled from the survey wall**. :::info Enabled by TapResearch Standalone profiling is set up by the TapResearch team. **Reach out to your account manager** to enable it for your app. ::: ## What it does You place the profiler as its own experience — a quick onboarding step, for example — where users answer demographic questions and then return to your app. No survey required. Once a user is profiled, they won't be shown the standalone profiler again. ## Why decouple it - **Collect profile data up front** — build a user's profile during onboarding, before they ever reach a survey. Use this data to better understand your user base and optimize the survey experience. - **Sharper targeting, fewer screen-outs** — surveys (and [User Boosts](user-boosts)) match more accurately when a user is already profiled, so they hit fewer dead ends. - **A lighter entry point** — a short profiler is an easier first action than a full survey. ## Turn it on Standalone profiling is enabled per app by the TapResearch team and set up as a dedicated profiler placement. **[Reach out to your account manager](mailto:developers@tapresearch.com)** to get started. --- # Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/features/native-survey-tiles # Native Survey Tiles **Native Survey Tiles** (formerly *Survey Wall Preview*) let you present available surveys directly inside your app — as native tiles you render in your own UI, instead of opening the full survey wall. :::info SDK only Native Survey Tiles are delivered through the TapResearch SDK. They aren't available on the Hosted Link, Eligibility API, or Survey Feed API paths. ::: - **Custom UI** — build your own presentation with survey tiles or cards. - **Direct survey access** — users tap straight into the surveys they choose. ## Platform guides
iOS

Integrate Native Survey Tiles in your iOS app.

View guide → Android

Integrate Native Survey Tiles in your Android app.

View guide → React Native

Integrate Native Survey Tiles in your React Native app.

View guide → Unity

Integrate Native Survey Tiles in your Unity app.

View guide →
--- # Quick Questions Source: https://supply-docs.tapresearch.com/docs/3.x/features/quick-questions # Quick Questions **Quick Questions** are short, in-app questions you ask your **own** users — up to three per prompt, surfaced automatically through the SDK, separate from the market-research surveys on the Survey Wall. Use them to capture first-party signal: app sentiment, feature interest, an NPS-style rating, or anything you'd ask in a quick prompt. You can optionally reward users for answering. :::info SDK only Quick Questions are delivered through the TapResearch SDK. They aren't available on the Hosted Link, Eligibility API, or Survey Feed API paths. ::: ## How it works You build a Quick Question in the dashboard — multiple-choice or open-ended — and attach it to an **Automatic** placement. Matching users see it inline, with no button tap required, and when they answer your app receives a **Quick Question Response** through an SDK callback. That lets you react in the moment — for example, routing a happy user to an app-store review. The response is a `TRQQDataPayload`: the questions that were asked, the user's answers, and context like the placement, user identifier, locale, and timestamps. A simplified example: ```json { "survey_identifier": "YOUR_SURVEY_IDENTIFIER", "placement_tag": "YOUR_PLACEMENT_TAG", "user_identifier": "YOUR_USER_IDENTIFIER", "user_locale": "en", "questions": [ { "question_text": "This is the question", "question_type": "multiple_choice__single", "user_answer": { "value": "Answer F", "identifiers": ["78bf19ea…"] } } ], "complete": { "completed_at": "2025-10-13T22:31:24.869Z" } } ``` The in-app callback fires whether or not you've configured a server-to-server postback, so you can handle answers client-side without waiting on your backend. Responses can also be ingested by postback, CSV export, or the reporting API. ## Set it up Quick Questions are built in the dashboard and handled per platform: - **[Quick Questions how-to](../3.2-only/quick-question-how-to)** — build and launch one. - **[Handling Quick Question responses](../3.2-only/handling-quick-question-responses)** — the full payload and every way to ingest it. --- # Setup Source: https://supply-docs.tapresearch.com/docs/3.x/setup # Setup A checklist to get your app ready in the dashboard. Create your account, choose how you'll integrate, then set up your app for that path. ## 1. Create your account Don't have a TapResearch account yet? [Sign up](https://www.tapresearch.com/suppliers/sign_up) — it's quick. Choose the **Publisher/Developer** option. You'll need an account and an **API token** to integrate. Account approval typically takes a few business hours. :::note Waiting for review? We typically review new accounts within a few hours. Need access sooner? Email [developers@tapresearch.com](mailto:developers@tapresearch.com). ::: ## 2. Choose your integration Decide how you'll integrate **before** you create your app — your choice determines how the app is set up. Compare the four paths and pick the one that fits your product. → [Choose your integration](choose-your-integration) ## 3. Create your app Create your app from the [Supplier Dashboard](https://www.tapresearch.com/supplier_dashboard/overview) and select the **integration path** you chose above. That configures the app correctly for you behind the scenes. - **SDK** — select each platform you ship on (**iOS**, **Android**, or both). Each platform gets its own **API token**, so use the matching token in each build. - **Hosted Link**, **Eligibility API**, or **Survey Feed API** — **one app is all you need.** The link or endpoint is the same across every surface (mobile *and* web), so a single app and token covers them all. You can create separate apps for different paths. ## 4. Configure app settings - **Colors** — primary and secondary action colors for the survey wall (HEX codes). - **App Store identifier** — set it now, or leave it blank and fill it in later. - **Callback URL** — optional; called when a user completes a survey (each platform has its own, but they can share a value). See [Callbacks](basic-integration/sdk-integration/callbacks). ## 5. Add a currency A currency is **required** before you can create a placement. Set its name, display name, image, exchange rate, and (recommended) a profile reward. → [Currencies](operational-tools/currencies) · [Exchange rates](operational-tools/exchange-rate) · [Profile reward](operational-tools/profile-reward) ## 6. Create placements Placements are the slots where your content appears. Three defaults are created automatically; add more as needed and reference each by its code tag. → [Placements](operational-tools/placements) ## 7. Add a test device Until you go live, only **test devices** can access content — add one before you test. → [Testing](testing-and-qa) ## 8. Build your integration Now that your app is set up, follow your path's guide to build it. → [Choose your integration](choose-your-integration) ## 9. Test and go live Verify everything in test mode, then promote your app to live. → [Testing](testing-and-qa) --- ## Access the Publisher Dashboard Once you have an account, the [Publisher Dashboard](https://www.tapresearch.com/supplier_dashboard/) is your home for managing apps, updating settings, accessing your API token, and monitoring performance. --- # Choose your integration Source: https://supply-docs.tapresearch.com/docs/3.x/choose-your-integration # Choose your integration TapResearch offers **four integration paths**. They share the same survey supply and reward callbacks, but differ in how much you build versus how much TapResearch handles for you — and in how you check whether a user is eligible before you send them. Use the cards below to jump straight to a path, or compare them feature-by-feature in the [At a Glance](#at-a-glance) matrix. :::note Mobile vs. web The **SDK** is for native **mobile** apps (iOS / Android). On **web or desktop**, use the **Hosted Link**, **Eligibility API**, or **Survey Feed API** — they're a URL or a server-side call, so they work anywhere. :::
Recommended
SDK

Our full-featured native integration. The SDK renders survey placements in your app and handles eligibility, ranking, profiling, and reward callbacks for you. Its hidden-webview logic updates on TapResearch's side, so behavior improves without you shipping a new SDK release.

View Documentation →
Fastest
Hosted Link

The fastest, low-code path. Integrate a single URL — the same link for every user — and TapResearch handles eligibility and survey selection. There is no SDK to install and no API to call; you only template the link per user.

View Documentation →
Pre-send check
Eligibility API

A lightweight server-side API that tells you whether a user has an eligible survey before you show a call-to-action — a simple yes/no plus a hand-off link, not the survey list.

View Documentation →
Control + impanel
Survey Feed API

Full control over the experience. Fetch the surveys themselves{' '} and render them in your own UI, with profile / impanel support for a profile-first flow. This is the only path that impanels participants.

View Documentation →
## At a Glance
Comparison Hosted Link SDK Eligibility API Survey Feed API
What you build A button and the link An entry point and reward handling A server-side check + hand-off link Survey list UI, webview, reward handling
Survey runs in Mobile browser In-app, we render Mobile browser Your in-app webview
User stays in your app ✕ ✓ ✕ ✓
Control over presentation Low Low Low Full
Effort and time to launch Lowest Low Low Highest
Reward notifications Server-to-server In-app callback or server-to-server Server-to-server Server-to-server
Native Survey Tiles ✕ ✓ ✕ ✓
Quick Questions ✕ ✓ ✕ ✕
In-app promotions ✕ ✓ ✕ ✕
Who handles survey abandons We do We do We do You do
Eligibility check ✕ ✓ ✓ ✓
:::tip Choosing for mobile? If you're integrating a mobile app (**iOS or Android**), start with the **[SDK](/docs/3.x/basic-integration/integration-options)** path. It delivers the richest, most native experience — in-app survey tiles, Quick Questions, and automatic profiling — and its hidden-webview logic keeps improving without you shipping a new SDK release. ::: --- # 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). --- # 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. --- # Android Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/android :::tip What you'll do 1. [Install the SDK](#installation) 2. [Set up reward callbacks](#callbacks) 3. [Initialize the SDK](#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 :::warning targetSdk 35+ If your app's `targetSdk` is 35 or higher, use `tapsdk:3.5.2` or higher — Android 15 introduced a change that otherwise cuts off the top title bar of the Survey Wall. ::: __Example apps are available [here](https://github.com/TapResearch/demo-apps/tree/master/android)__ > Minimum SDK version: 23 Add the following to your build.gradle file Add the following to your app-level `build.gradle` file: ```groovy repositories { maven { url "https://artifactory.tools.tapresearch.io/artifactory/tapresearch-android-sdk/" } ... } ``` And then add the following to your dependencies section of your `settings.gradle` file ```groovy dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url "https://artifactory.tools.tapresearch.io/artifactory/tapresearch-android-sdk/" } } } ``` ### **3.6.4+***: Required for you to import the following dependencies. If you're using a kotlin project, add the following to your `build.gradle` file :::info The latest version available via artifactory can be seen [here](https://artifactory.tools.tapresearch.io/artifactory/tapresearch-android-sdk/com/tapresearch/tapsdk/) ::: If you're using a java project, add the following to your `build.gradle` file ```groovy implementation 'com.tapresearch:tapsdk:3.7.2' implementation 'androidx.lifecycle:lifecycle-process:2.6.1' implementation 'androidx.core:core-ktx:1.10.1' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0' implementation 'com.google.android.gms:play-services-appset:16.1.0' ``` Or if you're using a kotlin project ```kotlin implementation("com.tapresearch:tapsdk:3.7.2") implementation("androidx.lifecycle:lifecycle-process:2.6.1") implementation("androidx.core:core-ktx:1.10.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") implementation("com.google.android.gms:play-services-appset:16.1.0") ``` ## Integration ### Callbacks There are a few callbacks that you will need to provide to the SDK upon initialization. | Override | Description | Required? | |-----------------------------------------|------------------------------------------------------------------------------------------------|-----------| | `rewardCallback` (`TRRewardCallback`) | The callback hit every time a reward is received for your users. | No | | `errorCallback` (`TRErrorCallback`) | The error message channel. Used if the SDK experiences an issue. | Yes | | `sdkReadyCallback` (`TRSdkReadyCallback`) | Lets you know when the SDK is done booting. | No | | `contentCallback` (`TRContentCallback`) | Lets you know when the content (survey wall, banner, interstitial, etc) is shown or dismissed. | No | | `qqDataCallback` (`TRQQDataCallback`) | The callback hit every time a quick question is answered by your users. | No | **Reward callback** (`TRRewardCallback`): This is called when the user completes a survey and is passed an array of `TRReward`'s. If you are using in-app callbacks to return rewards, you'll need to have this set up before initialization. The SDK will respond with a TRReward object that contains the reward amount and currency code. **Error callback** (`TRErrorCallback`): This is called when there is an error and is passed an instance of `TRError`. **sdkReadyCallback** (`TRSdkReadyCallback`): (optional) This is called when the SDK is ready to show content. This is a good time to show content. **contentCallback** (`TRContentCallback`): (optional) This is called when the content is shown or dismissed. This is a good time to log analytics. ### SDK Initialization You will need to initialize the SDK with both a unique user identifier and an api token. We suggest initializing the TapResearch SDK as early in the boot process as possible (once the user identifier is available). The demo app initializes in the `onCreate` method of the `MainActivity`. Similarly, there is a callback for when the SDK is ready. This is called when the SDK has finished initializing and is ready to show content. We suggest showing content only when the SDK is ready. Optionally, you may pass an initOptions parameter through initialization for additional configuration. See example below and [Initializations Options](../../operational-tools/initialization-options.md). :::info If user attributes are known at SDK initialization, it is preferable to pass them through `initOptions` compared to using `sendUserAttributes`. This will result in quicker load times for targeted 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. ::: ```kotlin filename="MainActivity.kt" val myUserIdentifier = "public-demo-test-user" val myApiToken = getString(R.string.api_token) Log.d("MainActivity", "API Token: $myApiToken") Log.d("MainActivity", "User identifier: $myUserIdentifier") TapResearch.initialize( apiToken = myApiToken, // userIdentifier must be unique for your player! userIdentifier = myUserIdentifier, context = this@MainActivity, // optional, handle rewards here or pass null and call setRewardCallback to handle later rewardCallback = { rewards -> showRewardToast(rewards) }, errorCallback = { error -> showErrorToast(error) }, sdkReadyCallback = { Log.d(LOG_TAG, "Tap SDK initialized and ready") }, // optional, handle here or pass null and call setQuickQuestionCallback to handle later qqDataCallback = { quickQuestionPayload -> Log.d(LOG_TAG, "Quick Question completed payload: $quickQuestionPayload") }, // optional, can pass null initOptions = TapInitOptions( userAttributes = hashMapOf( "is_vip" to 1, "something_else" to 2, ), clearPreviousAttributes = true, ), ) ``` ```java filename="MainActivity.java" // Your api token will likely be hardcoded in your app, but for the sake of this example String myUserIdentifier = "public-demo-test-user"; TapResearch.INSTANCE.initialize( myApiToken, myUserIdentifier, // user identifier must be unique for your player! MainActivity.this, // optional, handle rewards here or pass null and call setRewardCallback to handle later (rewards) -> { showRewardToast(rewards); }, (error) -> { showErrorToast(trError); }, () -> { Log.d(LOG_TAG, "SDK is ready"); // now that the SDK is ready, we can show content and send user attributes sendUserAttributes(); // showContent(); }, // optional quick question completed handler, can pass null here and call setQuickQuestionCallback later (quickQuestionPayload) -> { Log.d(LOG_TAG, "Quick Question was completed. Payload: " + quickQuestionPayload); }, // optional, can pass in options or null new TapInitOptions( userAttributes, true // Optional parameter to clear previous user attributes ) // Optional initialization options ); ``` ### Checking if SDK is ready to use In addition to waiting for the `sdkReadyCallback` via `initialize` (which is our recommended method), you can also determine if the SDK is ready to use by checking `isReady` ```kotlin TapResearch.isReady() // Example if (TapResearch.isReady() == false) { ... } else { ... } ``` ```java TapResearch.INSTANCE.isReady() // Example if (TapResearch.INSTANCE.isReady() == false) { ... } else { ... } ``` #### Reward callback setup additional information The **reward callback** can be set in two ways; via `initialize` and also via `setRewardCallback`. It is required for apps using in-app callbacks for rewards. Here we are just defining something that you are able to do with the rewards that are returned. ```kotlin filename="MainActivity.kt" // this can be set via initialize and also via setRewardCallback method rewardCallback = { rewards -> showRewardToast(rewards) } TapResearch.setRewardCallback(rewardCallback) private fun showRewardToast(rewards: MutableList) { var rewardCount = 0 for (reward: TRReward in rewards) { reward.rewardAmount?.let { rewardCount += it } } val currencyName = rewards.first().currencyName val eventType = rewards.first().payoutEventType Toast.makeText( this@MainActivity, "Congrats! You've earned $rewardCount $currencyName. Event type is $eventType", Toast.LENGTH_LONG, ).show() } // Note: in your Activity.onPause(), do: TapResearch.setRewardCallback(null) // Similarly, in your Activity.onResume(), do: TapResearch.setRewardCallback(your-reward-listener) ``` ```java filename="MainActivity.java" TapResearch.INSTANCE.setRewardCallback(your-reward-listener); private void showRewardToast(List rewards) { int rewardCount = 0; for (TRReward reward : rewards) { if (reward.getRewardAmount() != null) { rewardCount += reward.getRewardAmount(); } } String currencyName = rewards.isEmpty() ? "" : rewards.get(0).getCurrencyName(); String eventType = rewards.isEmpty() ? "" : rewards.get(0).getPayoutEventType(); Toast.makeText(MainActivity.this,"Congrats! You've earned " + rewardCount + " " + currencyName + ". Event type is " + eventType,Toast.LENGTH_LONG).show(); } // Note: in your Activity.onPause(), do: TapResearch.INSTANCE.setRewardCallback(null); // Similarly, in your Activity.onResume(), do: TapResearch.INSTANCE.setRewardCallback( your-reward-listener ); ``` #### Reward object > Note: in Java, you'll need to use the `getRewardAmount()` and `getCurrencyName()`, etc. methods 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 A TRReward object will look like this: ``` TRReward( transactionIdentifier="tap_pr_062ff34493dff7123691f79921978391", placementIdentifier="ffbd56740417114bc70c57f93151fa74", currencyName="tokens", rewardAmount=10, payoutEventType="profileReward", placementTag="normal-offer" ) ``` #### Quick Question callback setup additional information New to Quick Questions? See [Quick Questions](../../features/quick-questions) for what they are and the response payload. The **quick question callback** can be set in two ways; via `initialize` and also via `setQuickQuestionCallback`. It is optional for apps using quick questions that want to receive additional information when a user completes a quick question. Here we are just defining something that you are able to do with the quick question data that is returned. ```kotlin filename="MainActivity.kt" // this can be set via initialize and also via setQuickQuestionCallback method val quickQuestionCallback = TRQQDataCallback { quickQuestionPayload -> Log.d(LOG_TAG, "Quick Question payload data: $quickQuestionData") } TapResearch.setQuickQuestionCallback(quickQuestionCallback) // important: when you no longer need to wait for quick question data, such as Activity onPause, do: TapResearch.setQuickQuestionCallback(null) ``` ```java filename="MainActivity.java" // this can be set via initialize and also via setQuickQuestionCallback method TRQQDataCallback quickQuestionCallback = new TRQQDataCallback() { @Override public void onQuickQuestionDataReceived(QQPayload qqPayload){ Log.d(LOG_TAG,"quick question data received: "+qqPayload); } }; TapResearch.INSTANCE.setQuickQuestionCallback(quickQuestionCallback) // important: when you no longer need to wait for quick question data, such as Activity onPause, do: TapResearch.INSTANCE.setQuickQuestionCallback(null) ``` #### Quick Question data payload object A QQPayload object will look like this: ``` data class QQPayload( val surveyIdentifier: String, val appName: String, val apiToken: String, val sdkVersion: String, val platform: String, val placementTag: String, val userIdentifier: String, val userLocale: String, val seenAt: String, val complete: Complete?, val questions: List, val targetAudience: Array>?, ) ``` #### Error callbacks You can use the error callback to log or report errors. > See the [Error Codes](./sdk-error-codes) page for more information on the error codes ```kotlin filename="MainActivity.kt" // This is set in the initialization callback errorCallback = { trError -> showErrorToast(trError) } ``` ...and the error toast ```kotlin filename="MainActivity.kt" private fun showErrorToast(trError: TRError) { Toast.makeText( this@MainActivity, "Error: ${trError.description}", Toast.LENGTH_LONG, ).show() } ``` ```java filename="MainActivity.java" // This is set in the error callback new ErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { showErrorToast(trError); } } ``` ...and the error toast ```java filename="MainActivity.java" private void showErrorToast(TRError trError) { Toast.makeText(MainActivity.this, "Error: " + trError.getDescription(),Toast.LENGTH_LONG).show(); } ``` #### TRError object The error object contains the following properties: `code`: The error code. This can be used when debugging as there is an associated list of error codes and descriptions. `description`: The error message. This is a human readable description of the error. ### 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. There are two ways of sending user attributes. Firstly, you are able to pass them in initialization in initOptions. This works well any time you have user attributes available at sdk initialization time. *clearPreviousAttributes* allows you to remove any attributes previously sent to us for the user identifier. ```kotlin initialize( // ... initOptions = TapInitOptions( userAttributes = hashMapOf( "is_vip" to 1, "something_else" to 2, ), clearPreviousAttributes = true, ), //... ) ``` You can send user attributes as soon as the SDK is ready. This is done by calling `sendUserAttributes` with a `HashMap` of attributes. We suggest sending as many attributes as you think that you'll want to target on for surveys, special awards, etc. ```kotlin filename="MainActivity.kt" initialize( // ... sdkReadyCallback = object : TRSdkReadyCallback { override fun onTapResearchSdkReady() { Log.d(LOG_TAG, "SDK is ready") val userAttributes: HashMap = HashMap() userAttributes["age"] = 25 userAttributes["VIP"] = true userAttributes["name"] = "John Doe" userAttributes["first_seen"] = Instant.now().toString() TapResearch.sendUserAttributes( userAttributes, ) { trError -> showErrorToast(trError) } //... } }, //... ) ``` ```java filename="MainActivity.java" initialize( //... public void onTapResearchSdkReady() { Log.d(LOG_TAG, "SDK is ready"); HashMap userAttributes = new HashMap<>(); userAttributes.put("age", 25); userAttributes.put("VIP", true); userAttributes.put("name", "John Doe"); userAttributes.put("first_seen", Instant.now().toString()); TapResearch.INSTANCE.sendUserAttributes(userAttributes, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { Log.d(LOG_TAG, "Error sending user attributes: " + trError.toString()); } }); //... } //... ) ``` ### Setting the user identifier You can set the user identifier at any time. This is done by calling `setUserIdentifier` with a string. ```kotlin TapResearch.setUserIdentifier( userIdentifier = userId, errorCallback = object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { showErrorToast(trError) } }, ) ``` ```java TapResearch.INSTANCE.setUserIdentifier( userIdentifier, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { showErrorToast(trError); } } ); ``` ### Displaying a placement #### Checking if the placement is available A placement is a survey that is content displayed to the user. Ideally, you check to see if the placement can be shown before actually showing it. To find out if the content is available for the given user, call: `canShowContentForPlacement("placement tag")`. The error callback is optional. ```kotlin filename="MainActivity.kt" if (TapResearch.canShowContentForPlacement( placementTag, errorCallback = object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { trError.description?.let { Log.e("TRERROR", it) } } }, ) ) { //... } ``` ```java filename="MainActivity.java" if (TapResearch.INSTANCE.canShowContentForPlacement(placementTag, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { Log.d("TRLOG", "Whoops " + trError.toString()); } })) { //... } ``` #### Showing the placement The placement is shown by calling `showContentForPlacement("placement tag")`. The `onTapResearchContentShown`, `onTapResearchContentDismissed`, and `onTapResearchDidError` callbacks are optional. As hashmap of customParameters is also optional. ```kotlin filename="MainActivity.kt" TapResearch.showContentForPlacement( placementTag, customParameters, object : TRContentCallback { override fun onTapResearchContentShown(placement: TRPlacement) { tapResearchDidDismiss(placement) } override fun onTapResearchContentDismissed(placement: TRPlacement) { tapResearchContentShown(placement) } }, object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { showErrorToast(trError) } }, ) ``` ```java filename="MainActivity.java" TapResearch.INSTANCE.showContentForPlacement( placementTag, customParameters, new TRContentCallback() { @Override public void onTapResearchContentShown(TRPlacement trPlacement) { Log.d("TRLOG", "Content shown for placement " + trPlacement.toString()); } @Override public void onTapResearchContentDismissed(TRPlacement trPlacement) { Log.d("TRLOG", "Content dismissed for placement " + trPlacement.toString()); } }, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { showErrorToast(trError); } } ); ``` ### 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. ```kotlin filename="MainActivity.kt" val customParameters: HashMap = HashMap() customParameters["age"] = 25 customParameters["VIP"] = true customParameters["name"] = "John Doe" TapResearch.showContentForPlacement( placementTag, customParameters, object : TRContentCallback { override fun onTapResearchContentShown(placement: TRPlacement) { tapResearchDidDismiss(placement) } override fun onTapResearchContentDismissed(placement: TRPlacement) { tapResearchContentShown(placement) } }, object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { showErrorToast(trError) } }, ) ``` ```java filename="MainActivity.java" HashMap customParameters = new HashMap<>(); customParameters.put("age", 25); customParameters.put("VIP", true); customParameters.put("name", "John Doe"); TapResearch.INSTANCE.showContentForPlacement( selectedItem, placementCustomParameters, new TRContentCallback() { @Override public void onTapResearchContentShown(TRPlacement trPlacement) { Log.d("TRLOG", "Content shown for placement " + trPlacement.toString()); } @Override public void onTapResearchContentDismissed(TRPlacement trPlacement) { Log.d("TRLOG", "Content dismissed for placement " + trPlacement.toString()); } }, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { showErrorToast(trError); } } ); ``` ### 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. ::: Get placement details by calling `getPlacementDetails("placement tag", optional errorCallback)`. It will return a TRPlacementDetails object or null: ``` data class TRPlacementDetails( val name: String? = null, val contentType: String? = null, val currencyName: String? = null, val isSale: Boolean? = null, val saleType: String? = null, val saleEndDate: String? = null, // UTC date string. i.e. SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") val saleMultiplier: Float? = null, val salDisplayName: String? = null, val saleTag: String? = null, val bonusBarProgress: TRBonusBarProgress? = null, ) data class TRBonusBarProgress( val isActive: Boolean? = null, val currentCompletes: Int? = null, val bonusWindowEndAt: String? = null, // UTC date string. i.e. SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") val bonusTiers: List? = null, val error: TRError? = null, ) data class TRBonusTier( val tierNumber: Int? = null, val completesNeeded: Int? = null, val rewardAmount: Float? = null, val status: String? = null, ) ``` ```kotlin filename="MainActivity.kt" val placementDetails = TapResearch.getPlacementDetails("my-placement-tag") { error -> println(error) } ``` ```java filename="MainActivity.java" TapResearch.INSTANCE.getPlacementDetails( "my-placement-tag", new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { Log.d("Error", "Error getting placement details " + trError.toString()); } } ); ``` ### Android Proguard When creating a release version of your app, please use [our proguard rules](https://github.com/TapResearch/demo-apps/blob/master/android/FullFeatureSet/TapResearchKotlinFullDemo/app/proguard-rules.pro) to your existing proguard file. --- ## Need help? If you're experiencing issues or need assistance, reach out to [developers@tapresearch.com](mailto:developers@tapresearch.com). --- **Stay updated:** watch [TapResearch on GitHub](https://github.com/TapResearch) to get notified about new SDK releases. --- # Android Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/survey-wall-preview/android-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 * **Integration guide:** See the [Android SDK documentation](../basic-integration/sdk-integration/android) * **Example apps:** Available on [GitHub](https://github.com/TapResearch/demo-apps/tree/master/android) ### 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 :::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 The code below demonstrates how to: 1. Fetch surveys after SDK initialization 2. Set up a surveys refreshed listener (**required**) **Key points:** * Fetch surveys **only after** the SDK Ready event * Render the `TRSurvey` objects in your UI (see `handleMySurveys` function) * **Required:** Set the surveys refreshed listener to get updates when surveys change :::warning Attempting to fetch surveys **before** SDK Ready will result in an error and no surveys will be returned. ::: ```kotlin TapResearch.initialize( apiToken = "my-api-token", userIdentifier = "my-user-identifier", context = this@MainActivity, errorCallback = { error -> handleMyError(error) }, sdkReadyCallback = { Log.d(LOG_TAG, "SDK is ready") // Surveys can be fetched anytime after SDK Ready val surveys = TapResearch.getSurveysForPlacement("my-placement-tag"){} handleMySurveys(surveys) // your surveys handler; handle null or empty list // REQUIRED: Set your surveys refreshed listener TapResearch.setSurveysRefreshedListener(object: TRSurveysRefreshedListener{ override fun onSurveysRefreshedForPlacement(placementTag: String) { if (placementTag == "my-placement-tag") { val updatedSurveys = TapResearch.getSurveysForPlacement(placementTag) {} handleMySurveys(updatedSurveys) // your surveys handler; handle null or empty list } } }) }, rewardCallback = handleMyRewards, initOptions = null, qqDataCallback = null, ) ``` ```java TapResearch.INSTANCE.initialize( "my-api-token", "my-user-identifier", MainActivity.this, // context this::handleMyRewards, this::handleMyError, // SDK ready handler () -> { Log.d(LOG_TAG, "SDK Ready"); // Surveys can be fetched anytime after SDK Ready List surveys = TapResearch.INSTANCE.getSurveysForPlacement("my-placement-tag", null); handleMySurveys(surveys); // your surveys handler; handle null or empty list // REQUIRED: Set your surveys refreshed listener TapResearch.INSTANCE.setSurveysRefreshedListener(placementTag -> { if (placementTag.equals("my-placement-tag")) { List updatedSurveys = TapResearch.INSTANCE.getSurveysForPlacement(placementTag, null); handleMySurveys(updatedSurveys); } }); }, this::handleQuickQuestion, null //tapInitOptions ); ``` #### TRSurvey data payload object A TRSurvey object will look like this: ``` /** * TapResearch Survey - Use for the TapResearch survey preview feature. * * @property surveyId Unique survey id. Use when calling com.tapresearch.tapsdk.TapResearch.showSurveyForPlacement * @property lengthInMinutes Length of the survey in minutes. * @property rewardAmount Reward amount * @property currencyName Currency name * @property isSale Is this survey a sale? * @property saleEndDate Sale end date - UTC date string. i.e. SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") * @property saleMultiplier Sale multiplier. E.g. Earn 2.5x the normal, pre-sale reward amount. * @property preSaleRewardAmount The pre-sale reward amount, if this is a sale. * @property isHotTile Is this a 'Hot' tile? * @property category Category of the survey. * */ @OptIn(InternalSerializationApi::class) @Serializable data class TRSurvey( val surveyId: String? = null, val lengthInMinutes: Int? = null, val rewardAmount: Float? = null, val currencyName: String? = null, var isSale: Boolean? = null, var saleEndDate: String? = null, // UTC date string. i.e. SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") var saleMultiplier: Float? = null, var preSaleRewardAmount: Float? = null, var isHotTile: Boolean? = null, var category: String? = null, ) ``` ### Showing a survey You will need the **surveyId** from the **TRSurvey** object in order to show the survey: ```kotlin TapResearch.showSurveyForPlacement( placementTag = "my-placement-tag", surveyId = survey.surveyId, // from TRSurvey object customParameters = null, contentListener = object : TRContentCallback { override fun onTapResearchContentShown(placementTag: String) { // survey is being shown } override fun onTapResearchContentDismissed(placementTag: String) { // survey was dismissed } }, errorListener = object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { // a rare error occurred while showing the survey } } ) ``` ```java TapResearch.INSTANCE.showSurveyForPlacement( "my-placement-tag", survey.surveyId, // from TRSurvey object null, // custom parameters new TRContentCallback() { @Override public void onTapResearchContentShown(String placementTag) { // survey is being shown } @Override public void onTapResearchContentDismissed(String placementTag) { // survey was dismissed } }, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { // a rare error occurred while showing the survey } } ); ``` ### Passing custom parameters If you need to pass custom parameters, please use: ```kotlin val customParameters = HashMap() customParameters["isVIP"] = 1 TapResearch.showSurveyForPlacement( placementTag = "my-placement-tag", surveyId = survey.surveyId, // from your TRSurvey object customParameters = customParameters, contentListener = object : TRContentCallback { override fun onTapResearchContentShown(placementTag: String) { // survey is being shown } override fun onTapResearchContentDismissed(placementTag: String) { // survey has been dismissed } }, errorListener = object : TRErrorCallback { override fun onTapResearchDidError(trError: TRError) { // rare error occurred while showing the survey } } ) ``` ```java HashMap customParameters = new HashMap<>(); customParameters.put("isVIP", 1); TapResearch.INSTANCE.showSurveyForPlacement( "my-placement-tag", survey.surveyId, // from your TRSurvey object customParameters, new TRContentCallback() { @Override public void onTapResearchContentShown(String placementTag) { // survey is being shown } @Override public void onTapResearchContentDismissed(String placementTag) { // survey has been dismissed } }, new TRErrorCallback() { @Override public void onTapResearchDidError(TRError trError) { // a rare error occurred while showing the survey } } ); ``` --- # TRError Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/tap-objects # TRError **Platform note:** This object is only available on Android. iOS uses `NSError` objects for errors. ## Properties * **`code`** (Int) — The error code. See [SDK Error Codes](./sdk-error-codes) for the full list. * **`description`** (String, Android) — The error message * **`localizedDescription`** (String, iOS) — The error message --- # React Native Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/react-native :::tip What you'll do 1. [Install the SDK](#installation) 2. [Initialize the SDK](#initialization) 3. [Set up callbacks](#sdk-callbacks-setup) 4. [Display a placement](#displaying-a-placement) Later sections cover user attributes, the user identifier, placement details, 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 :::warning targetSdk 35+ If your app's `targetSdk` is 35 or higher, use `react-native-tapresearch` 3.5.3 or higher — Android 15 introduced a change that otherwise cuts off the top title bar of the Survey Wall. ::: See latest versions at https://www.npmjs.com/package/react-native-tapresearch Run the following command: `$ yarn add react-native-tapresearch` Install the iOS pods. The TapResearch iOS framework ships with `react-native-tapresearch` and is linked automatically, so no manual pod entry is needed: `$ cd ios && pod install` In build.gradle of your app, add `maven { url "https://artifactory.tools.tapresearch.io/artifactory/tapresearch-android-sdk/" }` in the `allprojects/repositories` section: ```groovy allprojects { repositories { ... maven { url "https://artifactory.tools.tapresearch.io/artifactory/tapresearch-android-sdk/" } ... } } ``` ## Integration ### Import the TapResearchSDK components ```js import { PlacementCustomParamsProps, TapSdkAdapter, TapSdkUI, TapUserAttributes, TapSdkReward, } from 'react-native-tapresearch'; ``` ### Initialization Initialize the TapResearchSDK as early as possible so TapResearch can start getting surveys ready for your users. The `initializeSdk()` method only needs to be called once after the main component has been mounted. It's recommended that you use separate API tokens for iOS and Android so you can track performance metrics separately in the dashboard. :::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. ::: #### Parameters - apiKey - from settings on publisher dashboard - userIdentifier - unique identifier assigned to the current user of your app - userAttributes - map with string key and number or string value, used for targeting content - clearPreviousAttributes - boolean, remove all user attributes previously assigned to userIdentifier ```js // export type TapUserAttributes = Record; // TapSdkAdapter.initializeSdk(apiKey: string, userIdentifier: string, userAttributes: TapUserAttributes, clearPreviousAttributes: boolean) TapSdkAdapter.initializeSdk("YOUR_IOS_OR_ANDROID_API_TOKEN", "USER_IDENTIFIER", userAttributes, clearPreviousAttributes); ``` ### SDK Callbacks setup The SDK will call delegate functions to notify the app of certain events, for example to pass rewards or to notify the SDK has taken over the UI. Define the callback functions with the following signatures: ```js onDidReceiveRewards: (rewards: Array) => void; onDidReceiveError: (error: TapSdkError) => void; onContentShown: (contentMessage: any) => void; onContentDismissed: (contentMessage: any) => void; onTapResearchSdkReady: () => void; onDidReceiveQQResponse: (qqResponse: TRQQDataPayload) => void; ``` For example, a reward callback may look like this: ```js function doRewards(rewards: Array) { console.log('Received some rewards', rewards); } ``` Make sure to render the TapSDK component with the previously defined callbacks as its properties: ```html ``` #### Receiving Rewards In the reward callback ```onDidReceiveRewards: (rewards: Array) => void;``` you will receive an array of `TapSdkReward` objects which contain information about each reward such as amount and currency name: ```js export type TapSdkReward = { transactionIdentifier: string; placementTag: string; placementIdentifier: string; currencyName: string; payoutEvent: string; rewardAmount: number; }; ``` ### isReady SDK Method As mentioned above, you can determine when the SDK is ready via the onTapResearchSdkReady callback. Alternatively, you can also call the isReady method: ```js TapSdkAdapter.isReady() ``` It may look something like this (example in Typescript): ```js const isReady = useCallback(() => { TapSdkAdapter.isReady() .then((result: any) => { console.log(`TapSdkAdapter.isReady() - Result: ${result}`); Toast.hide(); Toast.show({ type: result ? 'success' : 'error', text1: 'TapSdkAdapter.isReady?', text2: result.toString(), }); }) .catch((error: any) => { console.error(error); }); }, []); ``` ```js ``` ``` const getPlacementDetails = useCallback(() => { TapSdkAdapter.getPlacementDetails('earn-center') .then((result: TapSdkPlacementDetails) => { if (result != null) { alert( "Placement Details: " + result.name + " currency_name: " + result.currency_name + " is_sale: " + result.is_sale + " sale_type: " + result.sale_type + " sale_multiplier: " + result.sale_multiplier + " sale_display_name: " + result.sale_display_name + " sale_tag: " + result.sale_tag + " content_type: " + result.content_type + " sale_end_date: " + result.sale_end_date ); } }) .catch((error: any) => { console.error(error); }); }, []); ``` --- **Stay updated:** watch [TapResearch on GitHub](https://github.com/TapResearch) to get notified about new SDK releases. --- # React Native Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/survey-wall-preview/react-native-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 latest versions at https://www.npmjs.com/package/react-native-tapresearch **See [here](../basic-integration/sdk-integration/react-native) for general TapResearch SDK integration for React Native.** ### Before You Begin * You must be on SDK v3.4.1+ 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 `TapSdkAdapter.isReady()`) before calling any Native Survey Tiles interfaces :::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 ### Importing SDK Components ```js import { TapSdkAdapter, TapSdkUI, TapSdkSurvey, } from 'react-native-tapresearch'; ``` --- ### **hasSurveysForPlacement** Once the SDK is ready (`onTapResearchSdkReady` callback received), you can check if there are available surveys for a given placement tag: ```js /** * hasSurveysForPlacement(placementTag: string): Promise */ const hasSurveys = await TapSdkAdapter.hasSurveysForPlacement('wall-preview-placement'); ``` --- ### **getSurveysForPlacement** Returns the available surveys for the placement, if any. ```js /** * getSurveysForPlacement(placementTag: string): Promise */ const surveys = await TapSdkAdapter.getSurveysForPlacement('wall-preview-placement'); ``` --- ### **onSurveysRefreshedForPlacement** The SDK uses callback functions to notify apps of important events. In React Native, these callbacks are passed as props through the `` component. **Required:** When a placement refreshes with new surveys, `onSurveysRefreshedForPlacement` will be triggered so you can update your native UI. Configuring this callback properly is critical to ensuring users have up to date survey information and a good user experience, as survey inventory/availability is constantly changing. **SDK Callback Signatures** ```js onSurveysRefreshedForPlacement: (placementTag: string) => void; onDidReceiveRewards: (rewards: Array) => void; onDidReceiveError: (error: TapSdkError) => void; onContentShown: (placementTag: string) => void; onContentDismissed: (placementTag: string) => void; onTapResearchSdkReady: () => void; onDidReceiveQQResponse: (qqResponse: TRQQDataPayload) => void; ``` The `onSurveysRefreshedForPlacement` setup might look like this: ```js const onSurveysRefreshed = useCallback(async (placementTag: string) => { try { const newSurveys = await TapSdkAdapter.getSurveysForPlacement(placementTag); actions.setSurveys(newSurveys ?? []); // update UI with new surveys } catch {} }, []); // .. etc return ( ) ``` --- ### **showSurveyForPlacement** When a user taps on a survey tile, you will call `showSurveyForPlacement` using the appropriate placement tag and `survey_identifier` for that tile. The user will then be brought directly into the survey. ```js /** * showSurveyForPlacement(placementTag: string, surveyId: string): void */ const onTilePress = (surveyId: string) => { TapSdkAdapter.showSurveyForPlacement('wall-preview-placement', surveyId); }; return ( onTilePress(survey.survey_identifier)} /> ) ``` --- # Unity Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/unity :::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 platform setup, user attributes, the user identifier, 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. ::: ### Integration #### External Dependency Manager from Google Before importing **TapResearchSDK.unitypackage** make sure the latest EDM is imported into your Unity Download external-dependency-manager-latest.unitypackage **Assets -> Import Package -> Custom Package** > Note: If you already have EDM installed, skip this step or use this opportunity to update to the latest EDM. Import all files :::info **Previously Installed Android?** If you have previously integrated for Android, please **remove** any existing TapResearch aar files before importing the new package. **Assets/Plugins/Android/tapsdk.aar**
**Assets/Plugins/Android/*unitybridge.aar**
**Assets/TapResearchSDK/Plugins/Android/tapsdk.aar**
**Assets/TapResearchSDK/Plugins/Android/*unitybridge.aar** ::: ### Installation You can download the latest version of the [TapResearch Unity SDK](https://github.com/TapResearch/TapResearch-Unity-SDK/tags) on GitHub. You can follow our simple C# script [TapResearchExample.cs](https://github.com/TapResearch/demo-apps/blob/master/unity/SimpleUnityExample/Assets/TapResearchSDK/TapResearchSDKExample/TapResearchExample.cs) as an example or proceed with the steps below: #### Integrate .unitypackage Inside the TapResearchSDK directory, you will find **TapResearchSDK.unitypackage**. You can import the package by selecting **Assets > Import Package > Custom Package...** through the Unity menu. The TapResearch SDK includes the binaries for iOS and uses EDM to fetch Android binaries. ### Android Setup :::warning **targetSdk** If your app's targetSdk is 35 or higher, you will need to use version 3.5.3 or higher. Android 15 introduced a change that will cut off the top title bar of the Survey Wall. ::: #### Android EDM Settings We recommend using the **Default settings, EXCEPT Explode AARs**. **Assets -> External Dependency Manager -> Android Resolver -> Settings** **Uncheck Explode AARs** (no longer recommended by Google) After importing EDM and **TapResearchSDK.unitypackage** a dialog might pop-up asking to ENABLE AUTO-RESOLVE dependencies. Be sure to click ENABLE. We recommend auto-resolve because it makes integration much easier. #### Android API Levels Build Settings -> Player Settings -> Other Settings, set **Minimum API Level 24**. **Target API Level to HIGHEST INSTALLED (33 or higher recommended)**. #### Android Proguard Rules Copy and paste [our proguard rules](https://github.com/TapResearch/demo-apps/blob/master/android/kotlin/TapResearchKotlinDemo/app/proguard-rules.pro) to your existing proguard file located at: **Assets/Plugins/Android/proguard-user.txt** If you do not have this file, please create it with our proguard rules. #### Unity 2020 or earlier If you use Unity 2020 or earlier, please **uncheck** all **Minify** options including Debug, Release, and R8 under **Player Settings -> Publishing Settings** ### iOS Project Setup #### Frameworks The **TapResearchSDK includes a post-process script** that runs after compilation to add required frameworks and linker flags to the generated Xcode project for iOS. #### Build Post Process Script The Unity package includes a `BuildPostprocessoriOS.cs` script that performs some necessary modifications to the Xcode project that is generated when exporting a build from the Unity editor. This decompresses an archived TapResearchSDK xcframework folder into the Xcode project, updates some Xcode project settings and also sets a Unity package version string in the project's Info.plist file. :::info The package version string stored by the `BuildPostprocessoriOS.cs` script in `Info.plist` is used for SDK analytics reporting. If you do not use the included `BuildPostprocessoriOS.cs` script please make sure that your own scripts add the package version string into the Xcode project's `Info.plist` file! ::: The version string can be found in `TapResearch.cs` and is defined as `public const string SDKVersion = "X.Y.Z";`, please add this to your `Info.plist` as ``` TapResearchBridgeVersion X.Y.Z ``` You can also add ```csharp infoPlistRoot.SetString("TapResearchBridgeVersion", TapResearchSDK.SDKVersion); ``` to your existing Info.plist modification code or duplicate the code block from `BuildPostprocessoriOS.cs`: ```csharp string plistPath = buildPath + "/Info.plist"; PlistDocument plist = new PlistDocument(); plist.ReadFromString(File.ReadAllText(plistPath)); PlistElementDict rootDict = plist.root; rootDict.SetString("TapResearchBridgeVersion", TapResearchSDK.SDKVersion); File.WriteAllText(plistPath, plist.WriteToString()); ``` ### Callbacks #### SDK Ready callback example This function will be called when the SDK is ready to show content. It is a good time to update TapResearch with initial 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. ```csharp public void TapSdkReady() { Debug.Log("TapResearchSDK ready, going to send user attributes"); Dictionary userAttributes = new Dictionary(); userAttributes["some_string"] = "a string value"; userAttributes["some_number"] = "12"; userAttributes["another_number"] = 12; userAttributes["boolean"] = "true"; userAttributes.Add("another_string", "it's another string!"); DateTime now = DateTime.UtcNow; string iso8601String = now.ToString("o"); userAttributes["iso8601_date"] = iso8601String; TapResearchSDK.SendUserAttributes(userAttributes); } ``` #### Rewards callback info The SDK will check if the user has unredeemed rewards in the following events: * On SDK initialization * When the user exits TapResearch The `TapResearchRewardReceived` interface is the reward listener that will handle the new rewards that the player earned in a session. The reward information will be encapsulated in the `TRReward` object with the following methods: | Method name |Type| Description | |:----------------------|:----|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | TransactionIdentifier |String| The reward unique identifier | | CurrencyName |String| The virtual currency name | | PlacementIdentifier |String| The placement identifier that started the session identifier | | PlacementTag |String| The placement tag that started the session identifier | | RewardAmount |int| The reward amount in virtual currency. The value will automatically be converted to your virtual currency based on the exchange rate you specified in the [app settings](https://www.tapresearch.com/supplier_dashboard/overview). | | PayoutEvent |int| The action that the user was rewarded for. 0 - Profile Complete, 3 - Survey Complete. | **Setting the rewards callback** * You can set different reward callbacks in different scenes, when setting different callbacks for different scenes then the best practice is to set a scene's callback to null before loading a new scene. When setting a new reward callback the old one is replaced. * You can set one, and make sure that the object it is attached to is non-destructible so that it survives scene loads (see https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html). #### Rewards callback example This function will receive the rewards that the user earned in a session as an array of `TRReward` objects. You can set this before or after calling `Configure()`. ```csharp private void TapResearchRewardsReceived(TRReward[] rewards) { foreach (TRReward reward in rewards) { Debug.Log("Tap Rewards: You've earned " + reward.RewardAmount + " " + reward.CurrencyName + ". " + reward.TransactionIdentifier); } } ``` #### Content shown / dismissed callback setup :::info These are required but do not need to do anything ::: ```csharp public void TapContentShown(string placementTag) { Debug.Log("Survey Content Opened"); } public void TapContentDismissed(string placementTag) { Debug.Log("Survey Content Dismissed"); } ``` #### Error callback setup :::info This is required but does not need to do anything ::: Any error received from the TapResearch SDK will be passed back to this function ```csharp public void TapResearchDidError(string errorMessage) { Debug.Log("TapResearch Error: " + errorMessage); } ``` #### Quick Question response callback New to Quick Questions? See [Quick Questions](../../features/quick-questions) for what they are and the response payload. **Setting the Quick Question response callback** * You can set different Quick Question response callbacks in different scenes, when setting different callbacks for different scenes then the best practice is to set a scene's callback to null before loading a new scene. When setting a new reward callback the old one is replaced. * You can set one, and make sure that the object it is attached to is non-destructible so that it survives scene loads (see https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html). This function will receive a Quick Question response object. You can set this before or after calling `Configure()`. ```csharp private void TapQQResponseReceived(TRQQDataPayload payload) { Debug.Log("TapResearchExample: TRQQDataPayload received! placement: " + payload.PlacementTag + " userIdentifier: " + payload.UserIdentifier); } ``` ### Initialization Initialize the TapResearchSDK as early as possible once unique user identifier is known. Please note that the `Configure()` method only needs to be called once on app launch, make sure to call `DontDestroyOnLoad()` in the C# code for the class that implements the SDK's callbacks. > You must declare your callback functions prior to calling `Configure()`. Your iOS and Android apps have different API tokens. Use pre-processor directives so Unity knows which API token to use when you build your app. :::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. ::: ```csharp private static string tapAPIToken; private static string userIdentifier = "YOUR_UNIQUE_USER_ID"; private static string placementTag = "home-screen"; void Awake() { DontDestroyOnLoad(this); // Don't destroy this object on scene change #if UNITY_ANDROID tapAPIToken = "90d0d66d0213cf0245254e3d3b222e92"; // Public Test Android, replace with your own API token #elif UNITY_IPHONE tapAPIToken = "ed51703ae058f16bd196a81f9fc1bfcb"; // Public Test iOS, replace with your own API token #endif TapResearchSDK.TapContentShown = TapContentShown; TapResearchSDK.TapContentDismissed = TapContentDismissed; TapResearchSDK.TapResearchDidError = TapResearchDidError; TapResearchSDK.TapResearchSdkReady = TapSdkReady; TapResearchSDK.Configure(tapAPIToken, userIdentifier); // Set your reward handler when you are ready to receive the rewards TapResearchSDK.TapResearchRewardReceived = TapResearchRewardsReceived; // Set your quick question handler when you are ready to receive quick question result data TapResearchSDK.TapResearchQQResponseReceived = TapQQResponseReceived; } ``` ### Displaying a placement Wrapping in `CanShowContent` is optional but recommended. It will check if the placement is ready to be shown before attempting to show it. If the placement is not ready, it will return false and you can try again later. ```csharp public void showSurveyContent() { if (TapResearchSDK.CanShowContent(placementTag)) //DefaultTest Placement { TapResearchSDK.ShowContentForPlacement(placementTag); //DefaultTest Placement } } ``` ### Passing custom parameters > Parameters can only be ascii characters, Unicode is not supported. `ShowContentForPlacement(string placementTag, Dictionary customParameters);` ```csharp public void showSurveyContentWithParameters() { if (TapResearchSDK.CanShowContent(placementTag)) //CustomParam Placement { Dictionary customParameters = new Dictionary(); //Parameters customParameters["player_attribute"] = "my-vip"; customParameters["data_value"] = "integer"; customParameters["another_number"] = 12; customParameters.Add("another_string", "it's another string!"); TapResearchSDK.ShowContentForPlacement(placementTag, customParameters); } } ``` ### Send user attributes > User Attributes can only be ascii characters, Unicode is not supported. `SendUserAttributes( Dictionary userAttributes, bool clearPreviousAttributes);` Pass a dictionary and a boolean flag indicating if existing attributes should be cleared or not when setting, or updating, with new user attributes. :::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 ::: ```csharp Dictionary userAttributes = new Dictionary(); userAttributes["some_string"] = "a string value"; userAttributes["some_number"] = "12"; userAttributes["another_number"] = 12; userAttributes["boolean"] = true; userAttributes.Add("another_string", "it's another string!"); TapResearchSDK.SendUserAttributes(userAttributes, true); ``` #### Send User Attributes with Configure You can send a User Attributes dictionary when initializing the SDK. ```csharp Dictionary userAttributes = new Dictionary(); userAttributes["some_string"] = "a string value"; userAttributes["some_number"] = "12"; userAttributes["another_number"] = 12; userAttributes["boolean"] = true; userAttributes.Add("another_string", "it's another string!"); TapResearchSDK.ConfigureWithUserAttributes(tapAPIToken, tapPlayerUserId, userAttributes, true); ``` Configure with user attributes takes the following parameters: an API token string, player identifier string, user attributes dictionary and a boolean indicating wether the user attributes replace existing attributes or are an update to previously-set attributes. :::info If user attributes are known at SDK initialization, it is preferable to use `ConfigureWithUserAttributes` compared to `SendUserAttributes`. This will result in quicker load times for targeted content. ::: ### Setting a new player/user identifier A new user identifier can be set by passing a user identifier string to `UpdateCurrentUser()`: ```csharp TapResearchSDK.UpdateCurrentUser(new_user_identifier); ``` ### Checking if the SDK is ready The SDK lets the app know it is ready using a callback. Using this callback is described in the SDK Ready callback example above. You can also check if the SDK is ready using the `IsReady()` call: ```csharp if (TapResearchSDK.IsReady()) { ... } ``` ### Getting Placement Details Get placement details by calling: ``` TapResearchSDK.GetPlacementDetails(string placementTag) ``` It will return `TRPlacementDetails`: ``` public struct TRPlacementDetails { public readonly string Name public readonly string ContentType public readonly string CurrencyName public readonly bool IsSale public readonly string SaleType public readonly string SaleEndDate public readonly double SaleMultiplier public readonly string SaleDisplayName public readonly string SaleTag public readonly TRBonusBarProgress BonusBarProgress } public struct TRBonusBarProgress { public readonly bool IsActive public readonly int CurrentCompletes public readonly string BonusWindowEndAt public readonly TRBonusTier[] BonusTiers public readonly Nullable Error } public struct TRBonusTier { public readonly int TierNumber public readonly int CompletesNeeded public readonly int RewardAmount public readonly string Status } ``` ### Screen Orientation for Landscape Applications: TapResearch works better in portrait mode. Using Unity Player Settings to disable Portrait orientation may result in problems rendering some surveys. #### TapResearch Surveys and Quick Questions To make sure that TapResearch surveys display in portrait mode when you have locked your game in landscape mode set the `Screen.orientation` to `ScreenOrientation.Portrait`. Use co-routines or scene-loads to make sure it is in effect before opening any TapResearch content. We recommend switching into portrait orientation if you have locked your game to landscape. #### TapResearch Banners & Interstitials Banners and interstitials can work in any orientation, however when tapped they will go on to show surveys. For placements starting with a banner or interstitial we recommend switching to `Screen.orientation` to `ScreenOrientation.AutoRotation` with landscape left, right and portrait enabled for auto-rotation: ```csharp Screen.autorotateToPortrait = true; Screen.autorotateToLandscapeLeft = true; Screen.autorotateToLandscapeRight = true; ``` ### Unity example projects Example projects for Unity can be found in [GitHub](https://github.com/TapResearch/demo-apps). The examples show how to integrate the TapResearchSDK package and how orientation can be managed. #### Full feature-set Unity Example In the full feature-set example you can set your API tokens in two ways: setting variable in C# ([See source](https://github.com/TapResearch/demo-apps/blob/unity-triple-example/unity/FullUnityExample/Assets/TapResearchSDKExample/TapResearchExample.cs)), or using the Unity editor. If you would like to set your API token using the editor: 1. Open the example; 2. Open the `TapResearchTestScene` scene from the `TapResearchSDKExample` folder in the assets list; 3. Select `Canvas` in the hierarchy panel; 4. In the Inspector panel for `Canvas` scroll to the OS-specific token values and enter your API tokens. If you prefer to set the API token value in C#: 1. Open `TapResearchExample.cs` in your favorite editor 2. Set your API tokens in the Awake() function before `TapResearchSDK.Configure(tapAPIToken, tapPlayerUserId);` on [Line 57](https://github.com/TapResearch/demo-apps/blob/fbe74eb989e652dc7b90e0dd65b0c2426e5d58b9/unity/FullUnityExample/Assets/TapResearchSDKExample/TapResearchExample.cs#L57). #### Wall Unity Example This example contains a single simple scene and C# source file in `Assets/TapResearchWallDemo` that shows how to present TapResearch content and respond to show, dismiss and rewards callbacks. #### Native Survey Tiles Unity Example This is example shows how to integrate the Native Survey Tiles feature. It contains 2 scenes: 1. `TapResearchTestScene`: Starts the TapResearchSDK, shows how to check if a Native Survey Tiles placement has surveys; 2. `WallPreviewScene`: Shows how to use the `TRSurvey` objects returned by getting surveys using TapResearchSDK.GetSurveysForPlacement()`. #### Unity example project orientation handling We have included a simple orientation changer class [`TROrientationChanger.cs`](https://github.com/TapResearch/demo-apps/blob/v3.2.x-unity-orientation/unity/SimpleUnityExample/Assets/TapResearchSDK/TapResearchSDKExample/TROrientationChanger.cs) in the full-feature-set example project that illustrates how to change orientation. When our example starts we set (lock) to landscape orientation using ```csharp Screen.orientation = ScreenOrientation.LandscapeLeft; ``` When we are ready to show TapResearch content we use the included example orientation changer to change to portrait orientation: ```csharp orientationChanger.SetPortrait(OnOrientationChangedToPortrait); ``` In the completion handler for SetPortrait we show the placement: ```csharp private void OnOrientationChangedToPortrait() { TapResearchSDK.ShowContentForPlacement(placementTag); } ``` When the user dismisses the surveys we do the reverse to return to landscape orientation: ```csharp orientationChanger.SetLandscapeLeft(OnOrientationChangedToLandscapeLeft); ``` --- **Stay updated:** watch the [TapResearch Unity SDK on GitHub](https://github.com/TapResearch/TapResearch-Unity-SDK) to get notified about new releases. --- # Unity Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/survey-wall-preview/unity-surveys :::info **Notes on apps in test mode!** If your app is in test mode, you _MUST_ use a test user. These are defined in "Test Devices" on the dashboard. ::: ## Installation **See [here](../basic-integration/sdk-integration/unity) for TapResearch SDK Unity 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.1+ 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 `TapResearchSdkReady` callback or by checking `TapResearchSDK.IsReady()`) before calling any Native Survey Tiles interfaces :::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: ```csharp bool flag = TapResearchSDK.HasSurveys("placementTag"); ``` #### Getting available surveys Get the surveys for your placement. ```csharp TRSurvey[] surveys = TapResearchSDK.GetSurveysForPlacement("placementTag"); ``` #### 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). ```csharp TapResearchSDK.ShowSurveyForPlacement(survey.SurveyIdentifier, placementTag); ``` If you need to pass custom parameters you can use `ShowSurveyForPlacement` and pass a dictionary: ```csharp Dictionary customParameters = new Dictionary(); customParameters["player_attribute"] = "my-vip"; customParameters["data_value"] = "integer"; customParameters["another_number"] = 12; customParameters.Add("another_string", "it's another string!"); TapResearchSDK.ShowSurveyForPlacement(survey.SurveyIdentifier, placementTag, customParameters); ``` ### Callback Native Survey Tiles adds a **required** callback that lets you know when surveys have been refreshed. #### Setting the callback **Required:** You must set the delegate using `SetEnableSurveysRefreshedCallback`: ```csharp TapResearchSDK.SetEnableSurveysRefreshedCallback(true); ``` To enable the callback pass `true` to disable pass `false`. It is a good practice to disable the callback when it is not needed, for example when the game is no longer in the scene where you display the surveys. #### Handling the callback You will see calls to the callback function when surveys have been completed, after any TapResearch content has been dismissed. **Required:** Make sure to set the SDK's `TapResearchSurveysRefreshDelegate` callback: The callback is defined as `void TapResearchSurveysRefreshDelegate(string placementTag)`. To set the callback: ```csharp TapResearchSDK.TapResearchSurveysRefreshed = TapResearchSurveysRefreshed; ``` Implement the callback: ```csharp private void TapResearchSurveysRefreshed(string placementTag) { TRSurvey[] surveys = TapResearchSDK.GetSurveysForPlacement(placementTag); // Update displayed survey buttons } ``` --- # Flutter Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/flutter :::tip What you'll do 1. [Install the SDK](#installation) 2. [Set up callbacks](#callbacks) 3. [Initialize the SDK](#initialization) 4. [Display a placement](#displaying-a-placement) Later sections cover user attributes, the user identifier, placement details, and more. ::: ## Github The TapResearch flutter plugin with an example app is available on github: https://github.com/TapResearch/tapresearch_flutter_plugin/ ## Installation Add the `tapresearch_flutter_plugin` to your `pubspec.yaml` file: ```yaml dependencies: tapresearch_flutter_plugin: ^3.7.0--rc1 # Use the latest version ``` Or run the following command: `flutter pub add tapresearch_flutter_plugin` ## Integration :::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. ::: ### Callbacks The SDK uses callbacks to notify your app of certain events. You should implement these interfaces and provide them to the SDK. #### SDK Ready Callback This function will be called when the SDK is ready to show content. ```dart class MySdkReadyCallback implements TRSdkReadyCallback { @override void onTapResearchSdkReady() { print("TapResearch SDK is ready"); } } ``` #### Rewards Callback The SDK will notify you when a user has earned rewards. ```dart class MyRewardCallback implements TRRewardCallback { @override void onTapResearchDidReceiveRewards(List rewards) { for (var reward in rewards) { print("Received reward: ${reward.rewardAmount} ${reward.currencyName}"); } } } ``` #### Error Callback Handle any errors that may occur. ```dart void onError(TRError error) { print("TapResearch Error: ${error.description} (Code: ${error.code})"); } ``` #### Content Callbacks Notify your app when content is shown or dismissed. ```dart class MyContentCallback implements TRContentCallback { @override void onTapResearchContentShown(String placementTag) { print("Content shown for $placementTag"); } @override void onTapResearchContentDismissed(String placementTag) { print("Content dismissed for $placementTag"); } } ``` ### Initialization Initialize the TapResearch SDK as early as possible. It is required to use different API tokens for iOS and Android. :::tip The user identifier passed through initialization must be **unique** per user! 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. ::: ```dart import 'package:tapresearch_flutter_plugin/tapresearch_flutter_plugin.dart'; final _plugin = TapresearchFlutterPlugin(); void initTapResearch() { final String apiKey = Platform.isAndroid ? "YOUR_ANDROID_API_KEY" : "YOUR_IOS_API_KEY"; final String userIdentifier = "YOUR_UNIQUE_USER_ID"; _plugin.initialize( apiKey: apiKey, userIdentifier: userIdentifier, rewardCallback: MyRewardCallback(), sdkReadyCallback: MySdkReadyCallback(), errorCallback: onError, ); } ``` ### Checking if the SDK is ready You can check if the SDK is ready using the `isReady()` method: ```dart void checkSdkReady() async { bool isReady = await _plugin.isReady(); print("SDK Ready: $isReady"); } ``` ### Displaying A Placement You can check whether a placement is available by using `canShowContentForPlacement()`. It is recommended that you wrap `showContentForPlacement` calls with this check. ```dart void showPlacement(String placementTag) async { bool canShow = await _plugin.canShowContentForPlacement( tag: placementTag, errorCallback: onError, ); if (canShow) { _plugin.showContentForPlacement( tag: placementTag, contentCallback: MyContentCallback(), errorCallback: onError, ); } } ``` ### Passing custom parameters Custom parameters allow you to receive additional information about the user when they complete a survey. ```dart void showPlacementWithCustomParameters(String placementTag) { Map customParameters = { "custom_param_1": "value1", "is_vip": true, }; _plugin.showContentForPlacement( tag: placementTag, customParameters: customParameters, contentCallback: MyContentCallback(), errorCallback: onError, ); } ``` ### Sending User Attributes User attributes are used to target specific users with surveys. ```dart void sendUserAttributes() { Map userAttributes = { "age": 25, "gender": "female", "is_vip": true, }; _plugin.sendUserAttributes( userAttributes: userAttributes, clearPreviousAttributes: false, errorCallback: onError, ); } ``` ### Updating User Identifier You can set the user identifier at any time. ```dart void updateUserIdentifier(String newUserId) { _plugin.setUserIdentifier( userIdentifier: newUserId, errorCallback: onError, ); } ``` ### Getting Placement Details :::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. ::: Get placement details by calling `getPlacementDetails(placementTag, errorListener)`. ```dart void getPlacementInfo(String placementTag) async { TRPlacementDetails? details = await _plugin.getPlacementDetails( placementTag, errorListener: onError, ); if (details != null) { print("Placement Name: ${details.name}"); print("Is Sale Active: ${details.isSale}"); } } ``` #### TRPlacementDetails Class `TRPlacementDetails` contains the following properties: | Property | Type | Description | | :--- | :--- | :--- | | `name` | `String?` | The name of the placement. | | `contentType` | `String?` | The type of content associated with the placement. | | `currencyName` | `String?` | The name of the currency rewarded by this placement. | | `isSale` | `bool?` | Indicates if there is an active sale. | | `saleType` | `String?` | The category or type of the active sale. | | `saleEndDate` | `String?` | When the sale expires (UTC date string). | | `saleMultiplier` | `double?` | The reward multiplier during a sale. | | `saleDisplayName` | `String?` | The display name for the sale. | | `saleTag` | `String?` | A tag associated with the sale. | | `bonusBarProgress` | `TRBonusBarProgress?` | The user's progress toward a bonus. | #### TRBonusBarProgress Class | Property | Type | Description | | :--- | :--- | :--- | | `isActive` | `bool?` | Indicates if the bonus bar is currently active. | | `currentCompletes` | `int?` | The number of survey completions achieved toward the bonus. | | `bonusWindowEndAt` | `String?` | When the bonus window expires (UTC date string). | | `bonusTiers` | `List?` | A list of tiers available in the progress bar. | | `error` | `TRError?` | Error information if the progress could not be retrieved. | #### TRBonusTier Class | Property | Type | Description | | :--- | :--- | :--- | | `tierNumber` | `int?` | The sequence number of the tier. | | `completesNeeded` | `int?` | Number of completions required to reach this tier. | | `rewardAmount` | `double?` | The reward granted upon completing this tier. | | `status` | `String?` | The current status of the tier. | --- ## Grant Boost Boosts are temporary reward multipliers that can be granted to users. ```dart class MyGrantBoostListener implements TRGrantBoostResponseListener { @override void onGrantBoostResponse(TRGrantBoostResponse response) { if (response.success == true) { print("Boost granted successfully: ${response.boostTag}"); } else { print("Failed to grant boost: ${response.error?.description}"); } } } void applyBoost(String boostTag) { _plugin.grantBoost( boostTag, listener: MyGrantBoostListener(), ); } ``` **Stay updated:** watch [TapResearch on GitHub](https://github.com/TapResearch/tapresearch_flutter_plugin) to get notified about new SDK releases. --- # Flutter Native Survey Tiles Source: https://supply-docs.tapresearch.com/docs/3.x/survey-wall-preview/flutter-surveys ## Installation * **Integration guide:** See the [Flutter SDK documentation](../basic-integration/sdk-integration/flutter) * **Sample Survey Wall Preview app with custom tiles:** Available on [GitHub](https://github.com/TapResearch/tapresearch_flutter_plugin) ### Before You Begin * 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 `isReady()`) before calling any Native Survey Tiles interfaces :::info **Support** Any other questions? Reach out to your account manager or via Slack and we'll be happy to help! ::: ## Integration :::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. ::: The code below demonstrates how to: - Fetch surveys after SDK initialization - Set up a surveys refreshed listener (**required**) **Key points:** * Fetch surveys **only after** the SDK Ready event * Render the `TRSurvey` objects in your UI (see `SurveyWallPreviewScreen` in the [Sample App](https://github.com/TapResearch/tapresearch_flutter_plugin/blob/master/example/lib/main.dart) * **Required:** Set the surveys refreshed listener to get updates when surveys change :::warning Attempting to fetch surveys **before** SDK Ready will result in an error and no surveys will be returned. ::: ### Initialization Initialize the TapResearch SDK as early as possible. It is required to use different API tokens for iOS and Android. :::tip The user identifier passed through initialization must be **unique** per user! 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. ::: ```dart import 'package:tapresearch_flutter_plugin/tapresearch_flutter_plugin.dart'; final _plugin = TapresearchFlutterPlugin(); void initTapResearch() { final String apiKey = Platform.isAndroid ? "YOUR_ANDROID_API_KEY" : "YOUR_IOS_API_KEY"; final String userIdentifier = "YOUR_UNIQUE_USER_ID"; _plugin.initialize( apiKey: apiKey, userIdentifier: userIdentifier, rewardCallback: MyRewardCallback(), sdkReadyCallback: MySdkReadyCallback(), errorCallback: onError, ); } ``` ### Set the Surveys Refreshed Listener Implement `TRSurveysRefreshedListener` to be notified when surveys are refreshed and available for a placement. When surveys are refreshed, we recommend calling `getSurveysForPlacement()` so you can re-render your custom UI, as survey tile values may have changed. It could also be a good time to call `getPlacementDetails()` because the `placementDetails.contentType` may have changed. Possible values could be `profiler` or `survey_wall_preview` or another string value. ```dart class MySurveysRefreshedListener implements TRSurveysRefreshedListener { @override void onSurveysRefreshedForPlacement(String placementTag) { print("Surveys refreshed for $placementTag"); /* This is a good time to call getSurveysForPlacement() to re-render your SWP screen and / or getPlacementDetails() where the returned placementDetails.contentType can be 'profiler' or 'survey_wall_preview' or another string value */ } } ``` Provide the listener to the SDK: ```dart _plugin.setSurveysRefreshedListener(MySurveysRefreshedListener()); ``` ### Get Surveys for Placement Use `getSurveysForPlacement()` to fetch the list of available surveys for a placement tag. ```dart void fetchSurveys(String placementTag) async { List? surveys = await _plugin.getSurveysForPlacement(placementTag); if (surveys != null && surveys.isNotEmpty) { for (var survey in surveys) { print("Available Survey: ${survey.surveyId} - Reward: ${survey.rewardAmount}"); } } } ``` ### Show Survey When a user selects a survey from your custom UI, use `showSurveyForPlacement()` to open it. ```dart void openSurvey(String placementTag, String surveyId) { _plugin.showSurveyForPlacement( tag: placementTag, surveyIdentifier: surveyId, contentCallback: MyContentCallback(), errorCallback: onError, ); } ``` ### TRSurvey Class | Property | Type | Description | | :--- | :--- | :--- | | `surveyId` | `String?` | Unique identifier for the survey. | | `lengthInMinutes` | `int?` | Estimated time to complete in minutes. | | `rewardAmount` | `double?` | The reward amount for completion. | | `currencyName` | `String?` | The name of the currency. | | `isSale` | `bool?` | Indicates if a sale is active for this survey. | | `isHotTile` | `bool?` | Indicates if this is a high-priority survey. | --- ### Getting Placement Details :::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. ::: Get placement details by calling `getPlacementDetails(placementTag, errorListener)`. ```dart void getPlacementInfo(String placementTag) async { TRPlacementDetails? details = await _plugin.getPlacementDetails( placementTag, errorListener: onError, ); if (details != null) { print("Placement Name: ${details.name}"); print("Is Sale Active: ${details.isSale}"); } } ``` See [Set the Surveys Refreshed Listener](#set-the-surveys-refreshed-listener) Example Survey Wall Preview screen with custom tiles can be seen at (SurveyWallPreviewScreen class): https://github.com/TapResearch/tapresearch_flutter_plugin/blob/master/example/lib/main.dart #### TRPlacementDetails Class `TRPlacementDetails` contains the following properties: | Property | Type | Description | | :--- | :--- | :--- | | `name` | `String?` | The name of the placement. | | `contentType` | `String?` | The type of content associated with the placement. | | `currencyName` | `String?` | The name of the currency rewarded by this placement. | | `isSale` | `bool?` | Indicates if there is an active sale. | | `saleType` | `String?` | The category or type of the active sale. | | `saleEndDate` | `String?` | When the sale expires (UTC date string). | | `saleMultiplier` | `double?` | The reward multiplier during a sale. | | `saleDisplayName` | `String?` | The display name for the sale. | | `saleTag` | `String?` | A tag associated with the sale. | | `bonusBarProgress` | `TRBonusBarProgress?` | The user's progress toward a bonus. | #### TRBonusBarProgress Class | Property | Type | Description | | :--- | :--- | :--- | | `isActive` | `bool?` | Indicates if the bonus bar is currently active. | | `currentCompletes` | `int?` | The number of survey completions achieved toward the bonus. | | `bonusWindowEndAt` | `String?` | When the bonus window expires (UTC date string). | | `bonusTiers` | `List?` | A list of tiers available in the progress bar. | | `error` | `TRError?` | Error information if the progress could not be retrieved. | #### TRBonusTier Class | Property | Type | Description | | :--- | :--- | :--- | | `tierNumber` | `int?` | The sequence number of the tier. | | `completesNeeded` | `int?` | Number of completions required to reach this tier. | | `rewardAmount` | `double?` | The reward granted upon completing this tier. | | `status` | `String?` | The current status of the tier. | --- --- # 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, 10 - Bonus Bar Reward.| |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` --- # Hosted Link Source: https://supply-docs.tapresearch.com/docs/3.x/hosted-link/overview # Hosted Link The Hosted Link is the fastest, **low-code** way to connect your users to TapResearch surveys. You add a call-to-action in your app that opens a single URL, and TapResearch handles eligibility, survey selection, and presentation on our side. There's no SDK to install and no API to call. :::tip Creating your app In the [Supplier Dashboard](https://www.tapresearch.com/supplier_dashboard/overview), create a new app and select **Hosted Link**. See the [Setup checklist](../setup) for the full flow. ::: ## How it works You embed one hosted link in your app or web experience. The link contains a few **placeholder tokens** that you swap out per user before sending the user to it — most importantly, a unique identifier for the user. 1. **Build the link per user.** Take the hosted link from your dashboard and substitute the placeholder tokens (such as the user identifier) with that user's real values. 2. **Send the user to the link.** When they tap your call-to-action, open the built link. 3. **TapResearch handles the rest.** Eligibility, targeting, and survey matching all happen on TapResearch's side — there is no eligibility call for you to make. 4. **Rewards arrive via callback.** When a user earns a reward, TapResearch notifies your backend with a server-to-server postback. ## When to use it The Hosted Link trades control for speed — it's the fastest launch, with eligibility and presentation handled for you. Reach for a different path if you need: * **To keep users inside your app** — use the **SDK** path. The Hosted Link opens in the device's browser (see the warning below). * **Native survey tiles or in-app placements** — use the **SDK** path. * **An eligibility check before you send a user** — use the **Eligibility API** path. * **A survey feed you render yourself, or profile / impanel support** — use the **Survey Feed API** path. :::warning Open in a browser, not an in-app WebView The Hosted Link is designed to open in the device's browser. Loading it inside an in-app WebView is **not recommended** — the survey flow has nuances (notably how the close and abandon buttons need to behave) that are difficult to get right in a custom WebView. If you want to keep users inside your app, use the **SDK** instead — it handles the in-app survey experience automatically. ::: ## What you need * The **hosted link** for your placement, from the Publisher Dashboard. * A unique **user identifier** to substitute into the link for each user. No API token or eligibility request is required. The integration is light: add a call-to-action that opens the link, template the `uid` per user, and set up the reward [callback](callbacks.mdx) on your backend. ## Next steps * **[Implementation](setup.md)** — the URL format, the placeholder tokens to substitute per user, and the redirect behavior. * **[Callbacks](callbacks.mdx)** — receive reward notifications on your backend with the server-to-server postback. --- # Integrate with AI Source: https://supply-docs.tapresearch.com/docs/3.x/hosted-link/integrate-with-ai # Integrate Hosted Link with AI Copy the prompt below into your AI assistant — Claude, ChatGPT, Cursor, Codex, or whatever you use. It points the agent at our full, always-current Hosted Link reference, so it works from the real documentation instead of guessing. Hosted Link is the low-code path, so there's not much to build — mostly templating the link per user and handling the reward callback. Your agent can walk you through both. Review what it writes, and reach for the rest of these docs whenever you want the details yourself. ## After you integrate: verify it's working Hosted Link is low-code, so verification is quick. Complete your dashboard **Account Setup** first (currency, a placement with content, and a test device), then check the flow end-to-end on a **test device**. **Your AI agent can help** run and debug these steps, but it can't see your account — so your callback endpoint receiving the completion is what confirms the flow works. :::tip Use a real test session Open the link on a real test device — a test postback on its own won't exercise the flow. Add a test device first (**More → Test devices**). ::: 1. **Link opens the survey wall** — Open your templated Hosted Link on a test device; it should load the survey wall. 2. **Reward callback configured** — Set your server-to-server postback URL in your app settings. 3. **Survey complete recorded** — Complete a test survey through the link. 4. **Callback received & verified** — Confirm your endpoint receives a signed `GET` (validate the HMAC `sig`) and redeems the reward. (The dashboard's **Integration** tab shows only Account Setup for Hosted Link — there's no implementation checklist to watch, which is why the callback is your signal.) For deeper guidance, see [Testing & QA](/docs/3.x/testing-and-qa). --- # Implementation Source: https://supply-docs.tapresearch.com/docs/3.x/hosted-link/setup # Implementation This page covers how to use your hosted link: the URL format, the value you substitute per user, and what happens when a user opens the link. You need a TapResearch account and a **Hosted Link** app before you begin. Don't have an account yet? [Sign up](https://www.tapresearch.com/suppliers/sign_up), then grab your hosted link from the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview). ## The hosted link format Your dashboard generates the hosted link for you. It points at one of your placements — identified by your **app title identifier** and the **placement tag** in the path — and carries the user you're sending in the `uid` query parameter: ``` https://www.tapresearch.com/supply_api/v4/app_titles//placements//redirect?uid= ``` There is **no API token** in the link — the Hosted Link requires no authentication. The dashboard fills in the path for you; you only substitute the per-user identifier. ## Values to substitute The `` and `` are already filled in by the dashboard and are fixed per placement. Before you send a user to the link, fill in the user value: | Value | Location | Required | Description | |-------|----------|----------|-------------| | `` | `uid` query param | Yes | A persistent, unique identifier for the user. Swap this per user so rewards are attributed to the right account. Reuse the same value for a given user across sessions. | | `did` | Query param | No | An optional identifier for the user's device. | :::info One link, swapped per user The hosted link is the same for every user except for the per-user `uid`. Template the link once, then substitute the current user's value at the moment you hand them the link. ::: **Requirements:** * Use a stable `uid` for each user — the same value should map to the same user every time. * URL-encode any values that contain special characters. ## What happens when a user opens the link Opening the hosted link hands the user off to the TapResearch-hosted experience — you don't select, render, or redirect to anything yourself: 1. **We check what's available for the user.** Eligibility and targeting are handled entirely on TapResearch's side — there's no eligibility call for you to make. 2. **The user enters the survey experience.** First-time users go through a short profiling flow, then land on the TapResearch **survey wall**, where they browse and take their available surveys. 3. **Otherwise, they see a "no surveys available" message.** If the user isn't currently eligible for anything, they get a friendly, localized message instead. No reward is issued in this case. After a user completes a survey, TapResearch notifies your backend asynchronously — see [Callbacks](callbacks.mdx). ## Next steps * **[Callbacks](callbacks.mdx)** — set up the server-to-server postback so you're notified when a user earns a reward. --- # Callbacks Source: https://supply-docs.tapresearch.com/docs/3.x/hosted-link/callbacks ## Callbacks With the Hosted Link path, rewards are delivered to your backend with a **server-to-server postback**. TapResearch sends a request to your callback URL immediately after a user earns a reward, and you verify and credit it on your side. In-app callbacks do not apply to the Hosted Link path — there is no SDK to deliver them — so only the server-to-server postback is documented below. ## In-App Callbacks In-app callbacks report rewards to your app through the SDK, as 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 :::info In-app callbacks are the default. To receive rewards on your backend instead, configure the server-to-server postback described below. ::: ## Server-to-Server Postback The server-to-server postback is a `GET` request that TapResearch sends to your callback URL immediately after a user earns a reward. The reward details are passed as query parameters, and an HMAC-MD5 signature (`sig`) lets you verify the request originated from TapResearch. **Important:** With server-to-server callbacks your app is responsible for notifying users of the rewards they receive — TapResearch does not notify users with this method. ### Update Callback URL Set or update your callback URL in the **"Edit App"** section of the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview). **URL requirements:** * No spaces or special characters * Must return a 2xx response code to be considered valid ### Sample Request ``` https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4 ``` ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | uid | String | Persistent unique identifier for this user. | | 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. | | 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, 10 - Bonus Bar Reward. | | sig | String | The URL signature: an HMAC-MD5 generated from the query string minus the `sig` parameter. See [Signature Validation](#signature-validation) below. | :::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 [Signature Validation](#signature-validation)). ::: ### Callback Testing Use the **Test Callback** button in the dashboard 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. ::: ### Signature Validation For security, every callback request includes an HMAC-MD5 signature (`sig`) generated from the query string. Validate it on every request before you reward a user. **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. Grab your API secret from your main dashboard. Example: `26dcc0fc7b6208fdfeffaf19f627cb4a` 1. Start with the incoming callback request, for example: `https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4` 2. Isolate the query string, leaving out the question mark. 3. Strip out the `sig` parameter, including the leading ampersand (`&`). 4. URL-decode the remaining query string. 5. Run your API secret and the decoded query string through an HMAC-MD5 generator. 6. Compare your generated HMAC against the `sig` parameter value. If they match, record the complete and reward the user. ```ruby # Sample request URL url = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4" # Isolate query string query_string = url.gsub(/^(.*?)\?/, "") # Strip out the sig parameter stripped_query_string = query_string.gsub(/&sig.*/, "") # Decode URL decoded = URI.decode_www_form_component(stripped_query_string) # Generate HMAC-MD5 api_secret = "26dcc0fc7b6208fdfeffaf19f627cb4a" digest = OpenSSL::Digest.new("md5") md5 = OpenSSL::HMAC.hexdigest(digest, api_secret, decoded) puts md5 # 42cbd66af5b670bed293d9b01c06d3c4 ``` ```php ``` ```java private static final String API_SECRET = "26dcc0fc7b6208fdfeffaf19f627cb4a"; private static final String ALGO = "HmacMD5"; try { // URL String urlString = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4"; // Generate HMAC-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=") + 37); // 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); // 42cbd66af5b670bed293d9b01c06d3c4 } catch (Exception e) { e.printStackTrace(); } ``` ### Whitelisted IP Addresses To further harden your endpoint, restrict incoming callback requests to the following TapResearch source IP addresses: | IP Address | |------------| | 34.198.225.203 | | 3.88.121.21 | | 3.89.214.250 | | 3.92.129.242 | ### Retries If a callback fails, our system continues retrying to send the reward for 48 hours OR until a `2xx` response code is returned. --- # Eligibility API Source: https://supply-docs.tapresearch.com/docs/3.x/eligibility-api/overview # Eligibility API The **Eligibility API** is a pre-send eligibility check. You make a single server-side call before showing a survey call-to-action, and TapResearch tells you whether the user currently has an offer available. In short: **one call in, `has_offer` true/false out.** Gate your call-to-action on that answer. :::tip Creating your app In the [Supplier Dashboard](https://www.tapresearch.com/supplier_dashboard/overview), create a new app and select **Eligibility API**. See the [Setup checklist](../setup) for the full flow. ::: :::info The Eligibility API returns a **yes/no and a link** — it does **not** return a list of surveys. If you need to render survey tiles or run a profile-first impanel flow, use the **Survey Feed API** instead. ::: ## How it works 1. Before rendering your "Earn rewards" button (or any survey entry point), your backend calls the [has-offer check](has-offer). 2. TapResearch responds with `has_offer`: * **`true`** — the user has at least one offer waiting. Show the call-to-action and link the user to the returned `offer_url`. * **`false`** — the user has nothing available right now. Hide or disable the call-to-action so the user never taps into an empty wall. 3. When the user finishes an offer, TapResearch notifies your server through a [callback](callbacks). ## When to use it Use the Eligibility API when you want to: * Show a survey entry point **only** when there is something for the user to do. * Keep all logic on your server — no SDK, no client-side survey rendering. * Control the exact placement, copy, and styling of the call-to-action yourself. ## What you need * An **API token** for your app (from the Publisher Dashboard). * A stable **`user_identifier`** for each user. ## Next steps * [Eligibility check](has-offer) — the request and response in full. * [Callbacks](callbacks) — how rewards are reported back to your server. --- # Integrate with AI Source: https://supply-docs.tapresearch.com/docs/3.x/eligibility-api/integrate-with-ai # Integrate the Eligibility API with AI Copy the prompt below into your AI assistant — Claude, ChatGPT, Cursor, Codex, or whatever you use. It points the agent at our full, always-current Eligibility API reference, so it works from the real documentation instead of guessing. Your agent can implement the server-side eligibility check and the hand-off link for you. Review what it writes, and reach for the rest of these docs whenever you want the details yourself. ## After you integrate: verify it's working Run through these checks with help from your AI agent — it can write and run them and debug failures, but it can't 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. :::tip Use a real test session These checks register from real activity — a test postback on its own won't complete them. Add a test device first (**More → Test devices**). ::: 1. **API request received** — Make your first authenticated eligibility request. The Integration tab marks this once it sees the request. 2. **Eligibility handled** — Confirm you receive the yes/no plus the hand-off link and branch on it correctly. 3. **Reward callback configured** — Set your server-to-server postback URL. 4. **Survey complete & reward redeemed** — Send a test user through the hand-off link, complete a survey, and confirm your callback endpoint receives the signed reward and redeems it. 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). --- # Eligibility check Source: https://supply-docs.tapresearch.com/docs/3.x/eligibility-api/has-offer # Eligibility check Use this endpoint to check whether a user currently has a survey offer available **before** you show them a call-to-action. It returns a simple `has_offer` true/false plus the links you need to send the user into (or away from) the offer. ``` POST https://www.tapresearch.com/supply_api/v4/surveys/offer ``` All requests must be made over **HTTPS**. --- ## Request Send the parameters as form-encoded body fields. | Parameter | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------------------------------------------- | | `user_identifier` | String | ✅ | Your persistent, unique identifier for this user. | | `api_token` | String | ✅ | The API token for your app, found in the Publisher Dashboard. Must belong to an app created with the **Eligibility API** type. | | `client_ip` | String | ✅ | The end user's IP address. | ### Example Request ```bash curl -X POST https://www.tapresearch.com/supply_api/v4/surveys/offer \ -d "user_identifier=user-12345" \ -d "api_token=your-api-token" \ -d "client_ip=203.0.113.42" ``` --- ## Response The response is a JSON object. `has_offer` is the field to branch on; the URLs and `message_hash` are only meaningful when `has_offer` is `true`. | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------------------------------------------ | | `has_offer` | Boolean | `true` if the user has an offer available, `false` otherwise. | | `offer_reason` | Integer | A code explaining the result. **Always present.** See [Reason codes](#reason-codes). | | `reason_comment` | String | Human-readable explanation of `offer_reason`. **Always present.** | | `offer_url` | String | The URL to send the user to in order to start the offer. Present when `has_offer` is `true`. | | `abandon_url` | String | The URL to send the user to if they decline or abandon the offer. Present when `has_offer` is `true`. | | `message_hash` | Object | Details about the reward for completing the offer. See [message_hash](#message_hash) below. | ### `message_hash` A preview of the reward **range** currently available to this user, given current survey inventory. | Field | Type | Description | | ---------- | ------ | -------------------------------------------- | | `min` | String | Minimum reward amount currently available. | | `max` | String | Maximum reward amount currently available. | | `currency` | String | The name of the currency the user earns. | ### Example Response — offer available ```json { "has_offer": true, "offer_url": "https://www.tapresearch.com/supply_api/v3/surveys/start?token=...", "abandon_url": "https://www.tapresearch.com/supply_api/v3/surveys/abandon?token=...", "message_hash": { "min": "50", "max": "200", "currency": "gold" } } ``` ### Example Response — no offer ```json { "has_offer": false, "offer_reason": 19, "reason_comment": "No content available" } ``` --- ## Using the response * **`has_offer: true`** — Show your call-to-action and link the user to `offer_url`. Use `message_hash` to display the reward range (for example, "Earn 50–200 gold"). If the user backs out, send them to `abandon_url`. * **`has_offer: false`** — Do not show the call-to-action. There is nothing for the user to do right now. When the user completes the offer, TapResearch reports the reward to your server through a [callback](callbacks). --- ## Reason codes Every response includes `offer_reason` (an integer) and a matching `reason_comment`. Branch on the code, not the comment text. Common values: | Code | Meaning | | ---- | ------- | | 0 | Qualified — an offer is available (`has_offer: true`) | | 1 | Not qualified — no surveys available for this user right now | | 2 | App is in test mode — only registered test devices are eligible | | 4 | Not a supported country | | 5 | User is under the supported age | | 6 | Missing user identifier | | 8 | User is banned | | 19 | No active offers available | | 20 | User identifier is an anonymous device advertising ID, not a real user ID | | 22 | User is blacklisted | This is the common subset; other codes may appear. ## Errors Errors return `{ "error": "" }` with an HTTP status: | Status | When | | ------ | ---- | | `401 Unauthorized` | Missing or invalid `api_token` — body `{"error":"Unauthorized."}` | | `422 Unprocessable Entity` | `user_identifier` missing (`{"error":"user_identifier must be specified"}`), or the token isn't an Eligibility API app (`{"error":"This endpoint requires a v4 Eligibility API app."}`) | TapResearch may also return `503` during maintenance. --- # Callbacks Source: https://supply-docs.tapresearch.com/docs/3.x/eligibility-api/callbacks # Callbacks When a user completes an offer surfaced through the Eligibility API, TapResearch notifies your server with a server-to-server postback. Validate every callback before you reward the user. ## In-App Callbacks In-app callbacks report rewards to your app through the SDK, as 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 :::info In-app callbacks are the default. To receive rewards on your backend instead, configure the server-to-server postback described below. ::: ## Server-to-Server Postback The server-to-server postback is a `GET` request that TapResearch sends to your callback URL immediately after a user earns a reward. The reward details are passed as query parameters, and an HMAC-MD5 signature (`sig`) lets you verify the request originated from TapResearch. **Important:** With server-to-server callbacks your app is responsible for notifying users of the rewards they receive — TapResearch does not notify users with this method. ### Update Callback URL Set or update your callback URL in the **"Edit App"** section of the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview). **URL requirements:** * No spaces or special characters * Must return a 2xx response code to be considered valid ### Sample Request ``` https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4 ``` ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | uid | String | Persistent unique identifier for this user. | | 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. | | 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, 10 - Bonus Bar Reward. | | sig | String | The URL signature: an HMAC-MD5 generated from the query string minus the `sig` parameter. See [Signature Validation](#signature-validation) below. | :::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 [Signature Validation](#signature-validation)). ::: ### Callback Testing Use the **Test Callback** button in the dashboard 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. ::: ### Signature Validation For security, every callback request includes an HMAC-MD5 signature (`sig`) generated from the query string. Validate it on every request before you reward a user. **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. Grab your API secret from your main dashboard. Example: `26dcc0fc7b6208fdfeffaf19f627cb4a` 1. Start with the incoming callback request, for example: `https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4` 2. Isolate the query string, leaving out the question mark. 3. Strip out the `sig` parameter, including the leading ampersand (`&`). 4. URL-decode the remaining query string. 5. Run your API secret and the decoded query string through an HMAC-MD5 generator. 6. Compare your generated HMAC against the `sig` parameter value. If they match, record the complete and reward the user. ```ruby # Sample request URL url = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4" # Isolate query string query_string = url.gsub(/^(.*?)\?/, "") # Strip out the sig parameter stripped_query_string = query_string.gsub(/&sig.*/, "") # Decode URL decoded = URI.decode_www_form_component(stripped_query_string) # Generate HMAC-MD5 api_secret = "26dcc0fc7b6208fdfeffaf19f627cb4a" digest = OpenSSL::Digest.new("md5") md5 = OpenSSL::HMAC.hexdigest(digest, api_secret, decoded) puts md5 # 42cbd66af5b670bed293d9b01c06d3c4 ``` ```php ``` ```java private static final String API_SECRET = "26dcc0fc7b6208fdfeffaf19f627cb4a"; private static final String ALGO = "HmacMD5"; try { // URL String urlString = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4"; // Generate HMAC-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=") + 37); // 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); // 42cbd66af5b670bed293d9b01c06d3c4 } catch (Exception e) { e.printStackTrace(); } ``` ### Whitelisted IP Addresses To further harden your endpoint, restrict incoming callback requests to the following TapResearch source IP addresses: | IP Address | |------------| | 34.198.225.203 | | 3.88.121.21 | | 3.89.214.250 | | 3.92.129.242 | ### Retries If a callback fails, our system continues retrying to send the reward for 48 hours OR until a `2xx` response code is returned. --- # Survey Feed API Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/overview # Survey Feed API The **Survey Feed API** is the only TapResearch integration path where you send a participant's profile up front to **impanel** them, then fetch the surveys they qualify for and render those survey tiles inside your own UI. You stay in control of profiling, the survey list, and the participant experience end to end. :::tip Creating your app In the [Supplier Dashboard](https://www.tapresearch.com/supplier_dashboard/overview), create a new app and select **Survey Feed API**. See the [Setup checklist](../setup) for the full flow. ::: :::info Not the Eligibility API The Survey Feed API returns **survey data** for a profiled participant. It is a different product from the **Eligibility API**, which answers a yes/no "does this user have an offer?" check. The two were historically both referred to as the "Supply API" — they are not the same surface. If you only need an eligibility check before sending a user, use the Eligibility API instead. ::: ## How it works 1. **Look up countries & qualifications.** Call `/countries` and `/qualifications` to learn what profile data you can collect. 2. **Impanel the participant.** `POST` their profile to `/players/impanel` so TapResearch can match surveys — or skip this and let TapResearch profile them. 3. **Fetch surveys.** Call `/campaigns` to get the surveys the participant qualifies for. 4. **Render the tiles.** Display the surveys in your own UI and send the participant into one. 5. **Rewards arrive via callback.** When the participant earns a reward, TapResearch notifies your backend with a server-to-server postback. ### Two ways to profile
You pass profile data

Collect profiling data from your users and send it via the Impanel endpoint; TapResearch uses it to match surveys. Uses multiple endpoints — countries & qualifications, impanel, and campaigns.

Start with Impanel →
TapResearch handles profiling

Skip collecting profile data — TapResearch shows its built-in profiling experience. The only required endpoint is GET /campaigns.

Fetch surveys →
## When to use it Use the Survey Feed API when you want to: * Render survey tiles **inside your own UI**, fully styled by you. * Send participant profile data up front to target surveys (impanel) — or hand profiling to TapResearch. * Stay in control of the survey list and participant experience end to end. Reach for a different path if you need: * **A simple yes/no eligibility check** before sending a user — use the **Eligibility API**. * **The fastest low-code launch** — use the **Hosted Link**. * **Native mobile placements** — use the **SDK**. ## What you need The base URL for every endpoint in this API: ``` https://api.tapresearch.com/supply_api/v4 ``` * An **API token** for your app, from the Publisher Dashboard. * For some endpoints, a **security hash** (`sech`) generated server-side from your **API secret**. The secret should **never be sent** in any request — use it only to compute the `sech`. See [Impanel](./impanel) for how it's computed. * All requests must be made over **HTTPS**. ## Next steps | Endpoint | Method | Description | | ----------------------------------------------------- | ------ | --------------------------------------------------------- | | [`/countries`](./countries-qualifications) | GET | List supported countries and their IDs. | | [`/qualifications`](./countries-qualifications) | GET | Retrieve qualification questions per country. | | [`/players/impanel`](./impanel) | POST | Register a participant and store qualification responses. | | [`/campaigns`](./fetch-surveys) | GET | Fetch available surveys for a qualified participant. | After a participant completes a survey, rewards are reported to your backend — see [Callbacks](./callbacks) for the postback payload and signature validation. ## Support For credentials, redirect configuration, or questions about either integration path, contact your TapResearch account manager or email **[developers@tapresearch.com](mailto:developers@tapresearch.com)**. --- # Integrate with AI Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/integrate-with-ai # Integrate the Survey Feed API with AI Copy the prompt below into your AI assistant — Claude, ChatGPT, Cursor, Codex, or whatever you use. It points the agent at our full, always-current Survey Feed API reference, so it works from the real documentation instead of guessing. The Survey Feed API is the full-control path — fetching surveys, rendering them, and handling reward callbacks yourself — so there's the most to build here. Your agent can work through it with you. Review what it writes, and reach for the rest of these docs whenever you want the details yourself. ## After you integrate: verify it's working Run through these checks with help from your AI agent — it can write and run them and debug failures, but it can't 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. :::tip Use a real test session These checks register from real activity — a test postback on its own won't complete them. Add a test device first (**More → Test devices**). ::: 1. **API request received** — Make your first authenticated fetch-surveys request. The Integration tab marks this once it sees the request. 2. **Surveys rendered & entered** — Render a fetched survey and open it in your webview so a session reaches the survey. 3. **Reward callback configured** — Set your server-to-server postback URL. 4. **Survey complete & reward redeemed** — Complete a test survey and confirm your callback endpoint receives the signed reward and redeems it. 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). --- # Impanel / player profile Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/impanel # POST `/players/impanel` The `/players/impanel` endpoint ingests a participant's demographic and qualification data. This information is used by TapResearch's targeting system to determine which surveys the user is eligible for. Call this endpoint **before** requesting surveys so that [`/campaigns`](./fetch-surveys) can return only the campaigns the user qualifies for. --- ## Request ### Headers | Name | Required | Description | | --------------------- | -------- | --------------------------------------------------------- | | `X-Api-Token` | ✅ | Your API token. | | `X-User-Identifier` | ✅ | Unique user identifier. | | `X-Device-Identifier` | ❌ | Optional hardware/device ID for multi-device distinction. | --- ### Body Parameters (JSON) | Name | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------------------------------------------------------------- | | `country_id` | Integer | ✅ | Country ID from [`/countries`](./countries-qualifications). | | `profile_data` | Array | ✅ | Array of qualification responses from [`/qualifications`](./countries-qualifications). | | `ts` | Integer | ✅ | Current Unix timestamp (in seconds). | | `sech` | String | ✅ | SHA-256 hash of `user_identifier:timestamp`, signed with your API secret. | | `attributes_hash` | Object | ❌ | Key-value map of custom publisher-defined attributes used for segmentation. | --- ### Profile Data Format Each object in `profile_data` represents a qualification and its responses. | Field | Type | Description | | ------------- | ------- | ---------------------------------------------------------------------------- | | `question_id` | Integer | ID of the qualification question. | | `values` | Array | Selected `pre_code` integers or text values, depending on the question type. | --- ### Segmentation The optional `attributes_hash` field allows publishers to attach custom attributes to a user profile at impanel time. These attributes can be used for **segmentation and targeting** within TapResearch's system. * Keys are defined by the publisher * Values can be strings, numbers, or booleans * Attributes are persisted and may influence downstream campaign eligibility #### Example ```json "attributes_hash": { "is_new_user": true, "subscription_tier": "premium", "days_since_signup": 3 } ``` --- ### Example Request ```json { "country_id": 1, "profile_data": [ { "question_id": 42, "values": [25] }, { "question_id": 43, "values": [1, 2] }, { "question_id": 44, "values": ["94568"] } ], "attributes_hash": { "is_new_user": true, "subscription_tier": "premium" }, "ts": 1713456789, "sech": "a3f9b6c40b31..." } ``` --- ### Response A successful impanelment returns: ```json { "success": true } ``` If there's an issue, an error object is returned with details: ```json { "error": "Invalid security hash" } ``` --- ## Notes * The **API secret must never be included** in the request — only use it server-side to generate the `sech` hash. * Use the `pre_code` values from [`/qualifications`](./countries-qualifications) for Single select and Multi select data points. * The `sech` is computed as a SHA-256 digest of `user_identifier:ts` using your API secret. * Both `ts` and `sech` must be provided as JSON fields in the request body. * Call this endpoint **before** retrieving campaigns from [`/campaigns`](./fetch-surveys). * A `200 OK` response with `"success": true` confirms the participant has been impaneled successfully. * `attributes_hash` is optional — if omitted, no custom segmentation attributes are applied. --- **Next step:** [Fetch surveys →](./fetch-surveys) — retrieve the campaigns the participant qualifies for. --- # Fetch surveys Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/fetch-surveys # GET `/campaigns` Retrieve available campaigns (surveys) for a participant profile. This endpoint should be called after impaneling a participant to discover live survey opportunities. :::warning Configure a post-survey redirect URL first `/campaigns` requires your app to have a post-survey redirect URL configured (and an active placement) before it will return surveys. Without one, the call returns `422` with `A valid post-survey redirect URL is required.` — before any surveys are evaluated. See [Redirects](./redirects). ::: --- ## Request ### Headers | Name | Required | Description | | --------------------- | -------- | --------------------------------------------------------- | | `X-Api-Token` | ✅ | Your API token. | | `X-User-Identifier` | ✅ | Participant UUID. | | `X-Device-Identifier` | ❌ | Optional hardware/device ID for multi-device distinction. | ### Query Parameters | Name | Required | Description | | ------------- | -------- | -------------------------------------------------------- | | `user_ip` | ✅ | Participant IP address (e.g., `107.193.140.76`). | | `device_type` | ✅ | Device type — `1` = mobile, `2` = desktop, `3` = tablet. | --- ## Response The response returns a JSON object containing: | Field | Type | Description | | -------------------- | ------ | ---------------------------------------------- | | `surveys` | Array | List of available surveys for the participant. | | `player_eligibility` | Object | Participant eligibility status. | --- ### Surveys Object Each item in the `surveys` array contains: | Field | Type | Description | | ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------- | | `identifier` | String | Unique campaign identifier. | | `length_in_minutes` | Integer | Estimated survey duration. | | `conversion_rate` | Integer | Completion rate percentage. | | `reward_amount` | Integer | Reward amount for the participant on a full complete. | | `epc` | Float | Earnings per click **to you**, factoring in completes, DQs, and OQs. | | `currency_name` | String | Name of the in-app currency. | | `offer_entry_url` | String | URL to start the survey (redirect the participant here). | | `is_hot_tile` | Boolean/Integer | Indicates whether this survey's performance exceeds our internal threshold. | | `support_url` | String | TapResearch-hosted support contact URL for the player's current session. Present this as a "Get Help" link alongside the campaign in your UI. | --- ### Player Eligibility Object | Field | Type | Description | | ------------- | ------- | --------------------------------------------------------- | | `is_eligible` | Boolean | Indicates if the participant is eligible to take surveys. | | `reason` | Integer | Eligibility reason code. See [Reason codes](#reason-codes). | | `comment` | String | Human-readable eligibility comment. | #### Reason codes When `is_eligible` is `false`, `reason` tells you why. An empty `surveys` array almost always traces back to one of these. Common values: | Code | Meaning | | ---- | ------- | | 0 | Eligible | | 2 | App is in test mode — only registered test devices are eligible | | 4 | Not a supported country | | 5 | Under the supported age | | 6 | Missing user identifier | | 8 | Banned | | 12 | Missing device/user identifier | | 20 | User identifier is an anonymous device advertising ID, not a real user ID | | 22 | Blacklisted | This is the common subset; other codes may appear. --- ### Example Response ```json { "surveys": [ { "identifier": "4da2192594d2420648e5996e8bc1cb59", "length_in_minutes": 6, "conversion_rate": 66, "reward_amount": 68, "epc": 1.5, "currency_name": "points", "offer_entry_url": "https://www.tapresearch.com/router/offers/28e0f041675fb74b7ef254482ad1d502/pre_entry...", "is_hot_tile": 1 }, { "identifier": "7e41c40fa02ed36035b5955b58e52388", "length_in_minutes": 15, "conversion_rate": 8, "reward_amount": 232, "epc": 2.0, "currency_name": "points", "offer_entry_url": "https://www.tapresearch.com/router/offers/28e0f041675fb74b7ef254482ad1d502/pre_entry...", "is_hot_tile": 1 } ], "player_eligibility": { "is_eligible": true, "reason": 0, "comment": "Qualified" } } ``` --- ## Errors Errors return `{ "message": "", "error_code": }`: | `error_code` | Status | When | | ------------ | ------ | ---- | | 1000 | `401` | Invalid or missing `X-Api-Token` | | 1001 | `422` | Missing `X-User-Identifier` | | 1003 | `422` | A required header is missing | | 1002 | `503` | Temporarily unavailable (maintenance) | Other validation failures return `422` with a descriptive `message` and `error_code: null` — for example, `A valid post-survey redirect URL is required.` (see the prerequisite at the top of this page). --- ## Notes - Call this endpoint after successfully impaneling a participant. See [Impanel](./impanel). - Cache responses for up to **2 minutes** to reduce load. - Use the `offer_entry_url` to redirect participants into a live survey. - The `player_eligibility` block determines whether the user can proceed. If the participant is **not qualified**, the `surveys` array will be empty. - Surveys are already **ordered by our internal yield management algorithm**, so we recommend using them in the order returned. - **Reward amounts** represent what the participant earns **on a full survey complete**. The actual value to you may be lower since TapResearch also pays out for DQs/OQs based on time spent in the survey. - **EPC (earnings per click)** reflects **all payout types**—not just completes—giving you a holistic view of expected earnings. Use this field if you plan to **manage yield on your end** instead of relying solely on reward amount. --- **Next step:** [Callbacks →](./callbacks) — receive reward notifications on your backend when a participant completes a survey. --- # Fetch Surveys with Dynamic Currency Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/fetch-surveys-with-dynamic-currency # GET `/campaigns` Retrieve available campaigns (surveys) for a participant profile. This endpoint should be called after impaneling a participant to discover live survey opportunities. :::warning Configure a post-survey redirect URL first `/campaigns` requires your app to have a post-survey redirect URL configured (and an active placement) before it will return surveys. Without one, the call returns `422` with `A valid post-survey redirect URL is required.` — before any surveys are evaluated. See [Redirects](./redirects). ::: :::info Dashboard currency is still required, but unused A currency variant must still be configured in the [dashboard](../operational-tools/currencies). It will not be used — `currency_name` and `exchange_rate` below always take precedence. ::: --- ## Request ### Headers | Name | Required | Description | | --------------------- | -------- | ----------------------------------------------------------- | | `X-Api-Token` | ✅ | Your API token. | | `X-User-Identifier` | ✅ | Participant UUID. | | `X-Device-Identifier` | ❌ | Optional hardware/device ID for multi-device distinction. | ### Query Parameters | Name | Required | Description | | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------| | `user_ip` | ✅ | Participant IP address (e.g., `107.193.140.76`). | | `device_type` | ✅ | Device type — `1` = mobile, `2` = desktop, `3` = tablet. | | `currency_name` | ✅ | Display name of the currency for this request (e.g. `Coins`). 191 characters or fewer. | | `exchange_rate` | ✅ | Units of your currency per $1.00 USD (e.g. `1000`). Must be a finite decimal in `[10, 100,000,000)`; out-of-range values fail the session. Used to convert the participant's payout at completion. | --- ## Response The response returns a JSON object containing: | Field | Type | Description | | -------------------- | ------ | ----------------------------------------------- | | `surveys` | Array | List of available surveys for the participant. | | `player_eligibility` | Object | Participant eligibility status. | --- ### Surveys Object Each item in the `surveys` array contains: | Field | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------| | `identifier` | String | Unique campaign identifier. | | `length_in_minutes` | Integer | Estimated survey duration. | | `conversion_rate` | Integer | Completion rate percentage. | | `reward_amount` | Integer | Reward amount for the participant on a full complete. | | `epc` | Float | Earnings per click **to you**, factoring in completes, DQs, and OQs. | | `currency_name` | String | Name of the in-app currency. | | `offer_entry_url` | String | URL to start the survey (redirect the participant here). | | `is_hot_tile` | Boolean/Integer | Indicates whether this survey's performance exceeds our internal threshold. | | `support_url` | String | TapResearch-hosted support contact URL for the player's current session. Present this as a "Get Help" link alongside the campaign in your UI. | --- ### Player Eligibility Object | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------- | | `is_eligible` | Boolean | Indicates if the participant is eligible to take surveys. | | `reason` | Integer | Eligibility reason code. See [Reason codes](#reason-codes). | | `comment` | String | Human-readable eligibility comment. | #### Reason codes When `is_eligible` is `false`, `reason` tells you why. An empty `surveys` array almost always traces back to one of these. Common values: | Code | Meaning | | ---- | ------- | | 0 | Eligible | | 2 | App is in test mode — only registered test devices are eligible | | 4 | Not a supported country | | 5 | Under the supported age | | 6 | Missing user identifier | | 8 | Banned | | 12 | Missing device/user identifier | | 20 | User identifier is an anonymous device advertising ID, not a real user ID | | 22 | Blacklisted | This is the common subset; other codes may appear. --- ### Example Response ```json { "surveys": [ { "identifier": "4da2192594d2420648e5996e8bc1cb59", "length_in_minutes": 6, "conversion_rate": 66, "reward_amount": 68, "epc": 1.5, "currency_name": "points", "offer_entry_url": "https://www.tapresearch.com/router/offers/28e0f041675fb74b7ef254482ad1d502/pre_entry...", "is_hot_tile": 1 }, { "identifier": "7e41c40fa02ed36035b5955b58e52388", "length_in_minutes": 15, "conversion_rate": 8, "reward_amount": 232, "epc": 2.0, "currency_name": "points", "offer_entry_url": "https://www.tapresearch.com/router/offers/28e0f041675fb74b7ef254482ad1d502/pre_entry...", "is_hot_tile": 1 } ], "player_eligibility": { "is_eligible": true, "reason": 0, "comment": "Qualified" } } ``` --- ## Errors Errors return `{ "message": "", "error_code": }`: | `error_code` | Status | When | | ------------ | ------ | ---- | | 1000 | `401` | Invalid or missing `X-Api-Token` | | 1001 | `422` | Missing `X-User-Identifier` | | 1003 | `422` | A required header is missing | | 1002 | `503` | Temporarily unavailable (maintenance) | Other validation failures return `422` with a descriptive `message` and `error_code: null` — for example, `A valid post-survey redirect URL is required.` (see the prerequisite at the top of this page). --- ## Notes - Call this endpoint after successfully impaneling a participant. See [Impanel](./impanel). - Cache responses for up to **2 minutes** to reduce load. - Use the `offer_entry_url` to redirect participants into a live survey. - The `player_eligibility` block determines whether the user can proceed. If the participant is **not qualified**, the `surveys` array will be empty. - Surveys are already **ordered by our internal yield management algorithm**, so we recommend using them in the order returned. - **Reward amounts** represent what the participant earns **on a full survey complete**. The actual value to you may be lower since TapResearch also pays out for DQs/OQs based on time spent in the survey. - **EPC (earnings per click)** reflects **all payout types**—not just completes—giving you a holistic view of expected earnings. Use this field if you plan to **manage yield on your end** instead of relying solely on reward amount. --- **Next step:** [Callbacks →](./callbacks) — receive reward notifications on your backend when a participant completes a survey. --- # Countries & qualifications Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/countries-qualifications # Countries & qualifications Before you impanel a participant, fetch the supported countries and the qualification questions for that country. The values returned here are what you send as `country_id` and `profile_data` to the [Impanel](./impanel) endpoint. --- ## GET `/countries` List all countries where campaigns are available. ### Parameters | Type | Name | Required | Description | | --- | --- | --- | --- | | Header | `X-Api-Token` | Yes | Your API token. | ### Response Returns an array of countries. | Field | Description | | --- | --- | | `id` | Internal numeric country ID. | | `name` | Localized country name. | Use the `id` value when calling [Qualifications](#get-qualifications) and [Impanel](./impanel). --- ## GET `/qualifications` Retrieve all qualification questions available for a given country. These questions are used to collect profiling data before a participant is impaneled. ### Request #### Headers | Name | Required | Description | | --- | --- | --- | | `X-Api-Token` | ✅ | Your API token. | #### Query Parameters | Name | Required | Description | | --- | --- | --- | | `country_id` | ✅ | Country ID returned from [`/countries`](#get-countries). | ### Response An array of qualification objects. | Field | Type | Description | | --- | --- | --- | | `question_id` | Integer | Unique question identifier. | | `question_text` | String | Text of the qualification question. | | `answer_type` | String | One of `single select`, `multi select`, or `text`. | | `qualification_answers` | Array | For select types: array of `{ option_text, pre_code }`. | | `required` | Boolean | Whether the answer is mandatory. | #### Example Response ```json [ { "question_id": 50017, "question_text": "When were you born?", "country_id": 1, "name": "Birthday", "answer_type": "Text", "qualification_answers": [], "required": true }, { "question_id": 725, "question_text": "What are your hobbies and interests? Select all that apply.", "country_id": 1, "name": "Hobbies & Interests", "answer_type": "Multi select", "qualification_answers": [ { "option_text": "Arts and Crafts", "pre_code": 1 }, { "option_text": "Boating", "pre_code": 2 }, { "option_text": "Cooking", "pre_code": 3 } ], "required": false } ] ``` ### Notes - Use the `pre_code` values from `qualification_answers` when sending `profile_data` in the [`/players/impanel`](./impanel) request. - Supported answer types: - **Single select:** user selects one option. - **Multi select:** user can select multiple options. - **Text:** free-form input (e.g., ZIP code, birth year). --- **Next step:** [Impanel the participant →](./impanel) — register them with their qualification responses so TapResearch can match surveys. --- # Callbacks Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/callbacks # Callbacks TapResearch reports every reward to your backend through a server-to-server postback. Because the Survey Feed API has no in-app SDK surface, only the server-to-server postback applies here. ## In-App Callbacks In-app callbacks report rewards to your app through the SDK, as 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 :::info In-app callbacks are the default. To receive rewards on your backend instead, configure the server-to-server postback described below. ::: ## Server-to-Server Postback The server-to-server postback is a `GET` request that TapResearch sends to your callback URL immediately after a user earns a reward. The reward details are passed as query parameters, and an HMAC-MD5 signature (`sig`) lets you verify the request originated from TapResearch. **Important:** With server-to-server callbacks your app is responsible for notifying users of the rewards they receive — TapResearch does not notify users with this method. ### Update Callback URL Set or update your callback URL in the **"Edit App"** section of the [Publisher Dashboard](https://tapresearch.com/supplier_dashboard/overview). **URL requirements:** * No spaces or special characters * Must return a 2xx response code to be considered valid ### Sample Request ``` https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4 ``` ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | uid | String | Persistent unique identifier for this user. | | 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. | | 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, 10 - Bonus Bar Reward. | | sig | String | The URL signature: an HMAC-MD5 generated from the query string minus the `sig` parameter. See [Signature Validation](#signature-validation) below. | :::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 [Signature Validation](#signature-validation)). ::: ### Callback Testing Use the **Test Callback** button in the dashboard 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. ::: ### Signature Validation For security, every callback request includes an HMAC-MD5 signature (`sig`) generated from the query string. Validate it on every request before you reward a user. **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. Grab your API secret from your main dashboard. Example: `26dcc0fc7b6208fdfeffaf19f627cb4a` 1. Start with the incoming callback request, for example: `https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4` 2. Isolate the query string, leaving out the question mark. 3. Strip out the `sig` parameter, including the leading ampersand (`&`). 4. URL-decode the remaining query string. 5. Run your API secret and the decoded query string through an HMAC-MD5 generator. 6. Compare your generated HMAC against the `sig` parameter value. If they match, record the complete and reward the user. ```ruby # Sample request URL url = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4" # Isolate query string query_string = url.gsub(/^(.*?)\?/, "") # Strip out the sig parameter stripped_query_string = query_string.gsub(/&sig.*/, "") # Decode URL decoded = URI.decode_www_form_component(stripped_query_string) # Generate HMAC-MD5 api_secret = "26dcc0fc7b6208fdfeffaf19f627cb4a" digest = OpenSSL::Digest.new("md5") md5 = OpenSSL::HMAC.hexdigest(digest, api_secret, decoded) puts md5 # 42cbd66af5b670bed293d9b01c06d3c4 ``` ```php ``` ```java private static final String API_SECRET = "26dcc0fc7b6208fdfeffaf19f627cb4a"; private static final String ALGO = "HmacMD5"; try { // URL String urlString = "https://your-callback-url.example.com/postback?uid=developers%40tapresearch.com&tid=777ca23551a4a9173920c22e1ed7f4f3&cpid=tap_37939e4ede350f3a8d5149d2fcaa025e&payout_amount=191&payout_currency=gold&revenue=0.5&payout_type=3&sig=42cbd66af5b670bed293d9b01c06d3c4"; // Generate HMAC-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=") + 37); // 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); // 42cbd66af5b670bed293d9b01c06d3c4 } catch (Exception e) { e.printStackTrace(); } ``` ### Whitelisted IP Addresses To further harden your endpoint, restrict incoming callback requests to the following TapResearch source IP addresses: | IP Address | |------------| | 34.198.225.203 | | 3.88.121.21 | | 3.89.214.250 | | 3.92.129.242 | ### Retries If a callback fails, our system continues retrying to send the reward for 48 hours OR until a `2xx` response code is returned. ## Survey outcome parameter For every callback generated via the Survey Feed API, the `survey_outcome` parameter is included — an integer representing the participant's final status: | Value | Status | Description | | ----- | ------------ | ------------------------------------------------------------------- | | **1** | Complete | The participant successfully completed the survey. | | **2** | Disqualified | The participant was screened out based on targeting requirements. | | **3** | Over Quota | The survey reached its quota while the participant was in progress. | **Callbacks only fire for revenue-generating matches.** If a participant is disqualified or the survey reaches its quota, a callback is sent only when that outcome results in a partial reward (as configured in your App Settings). If no revenue is generated for the event, no postback is triggered. --- # Authentication & request signing Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/authentication # Authentication and Request Signing TapResearch’s Partner API uses two mechanisms to control access and verify request integrity: 1. **API Token** — identifies your partner account. Only required on endpoints that need authentication. 2. **API Secret** — used to generate `sech` signatures for specific flows. Never sent directly in a request. All Partner API endpoints require your API token for authentication, so this page focuses on when credentials are needed and how request signing works at a high level. --- ## API Token Your **API token** is required on **all** Partner API endpoints. Send it via header: | Header | Description | | ------------- | -------------------------------- | | `X-Api-Token` | Identifies your partner account. | This header is always required. --- ## User Identifiers For endpoints involving participant context, include the relevant identifiers: | Header | Required | Description | | --------------------- | -------- | ------------------------------------------------ | | `X-User-Identifier` | Yes | Unique user or device identifier (UUIDv4). | | `X-Device-Identifier` | Optional | Hardware/device ID for multi-device distinction. | --- ## Using the API Secret Your **API secret** is never transmitted. It is only used to generate signatures (`sech`) for two flows: 1. **Impanel Security Hash** (`/players/impanel`) 2. **Redirect Signing** (verifying redirect callbacks) For complete signing instructions, see: * **[Security Hash](#security-hash)** — below * **[Redirect Signing](./redirects.md)** --- ## Security Hash The security hash verifies authenticity for `/players/impanel`. ### How to Generate 1. Concatenate `user_identifier` and Unix timestamp with a colon. Example: `123e4567-e89b-12d3-a456-426614174000:1713456789` 2. Compute the SHA-256 HMAC using your **API secret**. 3. Send the following **as request params** (not headers): * `sech`: the hex-encoded HMAC digest * `ts`: the same Unix timestamp used in the payload 4. Hashes expire after **5 minutes**. #### Ruby Example ```ruby payload = "#{user_identifier}:#{timestamp}" digest = OpenSSL::HMAC.hexdigest('SHA256', api_secret, payload) ``` --- # Segmentation Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/segmentation # Segmentation Segmentation allows publishers to control which users are eligible for specific survey experiences by attaching **custom attributes** to a participant’s profile. These attributes are sent during impanelment and can be used to **target or restrict access to certain offers**. --- ## How Segmentation Works Segmentation is powered by the `attributes_hash` field on the [`/players/impanel`](./impanel.md) endpoint. When a user is impaneled: 1. You send custom attributes via `attributes_hash` 2. TapResearch stores these attributes on the user profile 3. These attributes are evaluated when determining survey eligibility 4. Only users matching an offer’s targeting rules will qualify ` `--- ## Sending Segmentation Data Segmentation attributes are passed as a JSON object: ```json { "attributes_hash": { "is_premium_user": true, "subscription_tier": "gold", "days_since_signup": 7 } } ``` ### Supported Value Types | Type | Example | | ------- | --------------------- | | Boolean | `true`, `false` | | Number | `1`, `42`, `3.14` | | String | `"gold"`, `"us_west"` | --- ## Example Use Cases Segmentation can be used to target surveys based on internal business logic or user state. ### Examples * Target only new users: ```json { "is_new_user": true } ``` * Segment by subscription tier: ```json { "subscription_tier": "premium" } ``` * Limit surveys to highly engaged users: ```json { "days_since_signup": 30 } ``` * Exclude users who have already completed onboarding: ```json { "completed_onboarding": false } ``` --- ## How Targeting is Applied Each offer can be configured with segmentation rules. When a user requests surveys: * Their stored attributes are evaluated against available offers * Only offers with matching criteria are considered eligible * If no segmentation rules are defined, the offer is available to all users --- ## When to Send Attributes * Attributes should be sent during [/players/impanel](./impanel.md). * You may update attributes over time by calling the endpoint again * Updates take effect immediately for future survey requests --- ## Best Practices * Use clear, consistent naming for keys (e.g. `is_premium_user`, not `premiumFlag`) * Keep attribute values simple and predictable * Prefer booleans and enums over free-form strings * Avoid sending sensitive or personally identifiable information (PII) * Only send attributes that are relevant for targeting decisions --- ## Notes * `attributes_hash` is optional — if not provided, no segmentation is applied * Attributes are stored per user and persist across sessions * Segmentation is evaluated automatically — no additional API calls are required * Attributes may influence which surveys are returned from [`/campaigns`](./fetch-surveys.md) --- # Pass-through values Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/pass-through # Pass-Through Values Pass-through values allow publishers to send custom key-value data when launching a survey. These values are stored and then returned on the **callback** when a reward is issued. This is useful for passing internal identifiers such as: * Session IDs * Placement IDs * Campaign IDs * User segmentation flags * Any custom reconciliation metadata --- ## Where to Append Pass-Through Values The **offer entry URL** is returned as one of the attributes in the `campaigns#index` API response. To use pass-through values: 1. Retrieve the offer entry URL from the `campaigns#index` response. 2. Append the `pass_through_values` parameter to that URL before redirecting the user. --- ## Parameter Format Append the following parameter to the end of the offer entry URL: ``` pass_through_values= ``` ### Encoding Requirements * The value must be a JSON object (key-value pairs). * The JSON string must be Base64 encoded using **strict encoding**. * The encoded value must be URL-safe. --- ## Example ### Original JSON ```json { "session_id": "abc123", "placement_id": "wall_top", "campaign": "spring_promo" } ``` ### Base64 Encoded ``` eyJzZXNzaW9uX2lkIjoiYWJjMTIzIiwicGxhY2VtZW50X2lkIjoid2FsbF90b3AiLCJjYW1wYWlnbiI6InNwcmluZ19wcm9tbyJ9 ``` ### Final Offer Entry URL ``` https://www.tapresearch.com/offers/identifier/pre_survey_entry?...&pass_through_values=eyJzZXNzaW9uX2lkIjoiYWJjMTIzIiwicGxhY2VtZW50X2lkIjoid2FsbF90b3AiLCJjYW1wYWlnbiI6InNwcmluZ19wcm9tbyJ9 ``` --- ## Callback Behavior When a reward callback fires: * The stored pass-through JSON is parsed. * Each key/value pair is merged into the callback payload. * Each value is returned as a **separate top-level parameter**. * Values are **not nested** under `pass_through_values`. ### Example Callback Payload ```json { "uid": "developers", "cpid": "tap_37939e4ede350f3a8d5149d2fcaa025e", "payout_amount": 191, "revenue": 0.5, "session_id": "abc123", "placement_id": "wall_top", "campaign": "spring_promo", "sig": "b438afd7426c743e777c387d7e2712d4" } ``` Each pass-through value appears as its own attribute in the callback. --- ## Error Handling If: * `pass_through_values` is not valid Base64, or * The decoded value is not valid JSON The values will be ignored and no custom attributes will be stored. --- ## Best Practices * Keep the JSON payload small. * Use stable, predictable keys. * Avoid sending sensitive information. * Ensure values are unique per click when needed for reconciliation. * Avoid using keys that conflict with existing callback parameters. --- --- # Redirect URLs Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/redirects # Redirect URLs After survey completion, participants are redirected to your supplied URL with signed parameters. The redirect URL can be handled in **two ways**: 1. **Placeholder substitution** (recommended for full control) 2. **Query parameter appending** (automatic fallback) In both cases, a SHA-256 HMAC signature (`sech`) is added to verify integrity. --- ## Parameters | Name | Description | |-----|-------------| | `status` | Click key state (see Redirect Statuses). | | `revenue` | Partner revenue amount (float). | | `reward` | Participant reward amount (float). | | `tid` | Publisher-provided session or match identifier. This should be unique per click and is returned exactly as received. | | `click_id` | TapResearch internal identifier for the click. | | `pass_through` | Base64-encoded JSON payload containing pass-through values originally provided during the survey request. | | `sech` | SHA-256 HMAC signature verifying the signed parameters. | --- ## Redirect Modes ### Placeholder Substitution If your redirect URL contains placeholders using curly braces (e.g., `{STATUS}`), those placeholders will be replaced before redirecting. Supported placeholders: * `{STATUS}` * `{REVENUE}` * `{REWARD}` * `{TID}` * `{CLICK_ID}` * `{PASS_THROUGH}` Example: ```text https://example.com/callback?status={STATUS}&tid={TID}&pt={PASS_THROUGH} ``` After substitution: ```text https://example.com/callback?status=1&tid=session_123&pt=eyJrZXkiOiJ2YWx1ZSJ9&sech= ``` **Important behavior** * Only placeholders **present in the URL** are included in the signature. * After substitution, the system: * Parses the resulting query parameters * Computes `sech` * Appends `sech` to the query string. --- ### Query Parameter Appending (No Placeholders) If your redirect URL does **not** contain placeholders, the system automatically appends all parameters: * `status` * `revenue` * `reward` * `tid` * `click_id` * `pass_through` Example base URL: ```text https://example.com/callback ``` Result: ```text https://example.com/callback?status=1&revenue=0.45&reward=50&tid=session_123&click_id=abc123&pass_through=eyJrZXkiOiJ2YWx1ZSJ9&sech= ``` In this mode, **all parameters are always signed**. --- ## Pass-Through Values The `pass_through` parameter contains **Base64-encoded JSON** representing metadata provided earlier in the survey request flow. Example decoded value: ```json { "sub_id": "user_456", "campaign": "spring_sale" } ``` Publishers may decode this value to recover their original tracking metadata. --- ## How Signing Works The system computes a SHA-256 HMAC using your API secret. ### Signing Order Canonical signing order: ```text status,revenue,reward,tid,click_id,pass_through ``` ### Data String Construction The signature is created by: 1. Taking only the relevant keys (depending on redirect mode). 2. Ordering them according to the canonical signing order above. 3. Joining their values with commas. Example: ```text 1,0.45,50,session_123,abc123,eyJrZXkiOiJ2YWx1ZSJ9 ``` If `tid` is blank, it is included as an empty value: ```text 1,0.45,50,,abc123,eyJrZXkiOiJ2YWx1ZSJ9 ``` ### Signature Generation ```text HMAC-SHA256(data_string, api_secret) ``` The resulting lowercase hex digest becomes the `sech` parameter. --- ## How to Validate the Redirect When you receive the redirect: 1. Extract the relevant parameters. 2. Reconstruct the comma-separated string in the correct order. 3. Compute the SHA-256 HMAC using your API secret. 4. Compare your result to the received `sech`. If they do not match: * The parameters were modified, or * The wrong API secret was used. --- # Redirect statuses Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/redirect-statuses # API Redirect Statuses When TapResearch redirects a participant back to your platform, we include a `status` value that explains **why** the participant returned. Redirects fall into three stages: session checks, pre-match checks, and post-match outcomes. For **session checks** and **pre-match checks**, all redirects return with a **blank `tid`** (match ID) because no reward is issued. **Post-match outcomes always include a `tid`** since most result in a reward and the value supports de-duplication. --- ## 1. Session Checks (Immediate Returns) These checks run **as soon as the participant loads the entry URL**. If anything looks suspicious — unusual activity patterns, mismatched regions, unsupported devices, or risky behavior — the participant is returned immediately. | Code | Status | Description | | ------ |---------------------------------|---------------------------------------------------------------------------| | **19** | ZIP / IP Mismatch | The participant’s ZIP code doesn’t match their detected IP region. | | **20** | Device/IP Activity Limit | This device or IP has exceeded safe activity levels in the past 24 hours. | | **21** | IP Blocked | The participant’s IP is blocked due to risk or abuse. | | **29** | IP Activity Limit (Completed) | This IP has too many recent completions across surveys. | | **30** | IP Activity Limit (Participant) | This participant’s IP has had too many sessions. | | **31** | Unsupported Device | The participant opened a mobile-only integration on desktop. | | **32** | Modified User Agent | The participant changed their browser/user agent mid-session. | | **33** | Underage | The participant does not meet the minimum age requirement. | | **34** | Quality Blocked | Past behavior indicates quality risks; session blocked. | | **38** | Fraud Blocked | Fraud-like signals detected. | | **52** | Location Validation Block | Participant failed location checks (used to detect VPNs/proxies). | --- ## 2. Pre-Match Checks (Before the Survey) These checks happen **after session checks pass** but **before** matching the participant to a live survey. | Code | Status | Description | | ------ | ----------------- | ---------------------------------------------------------------------------------------------------------- | | **5** | Unqualified | The participant did not qualify before entering the survey. | | **6** | Over Quota | The quota closed before the participant entered the survey. | | **8** | Traffic Throttled | System predicted the participant would hit traffic limits. | | **27** | Entry Term | The participant failed partner-level eligibility checks. *(Only pre-match code that may include a `tid`.)* | --- ## 3. Post-Match Outcomes (After Matching or Survey Interaction) These statuses reflect what happened **after** a match attempt or after the participant interacted with the survey. | Code | Status | Description | | ------ | --------------------- | ------------------------------------------------------------------- | | **1** | Complete | The participant completed the survey. | | **2** | Disqualified | The participant was screened out based on targeting requirements. | | **3** | Over Quota | The survey reached its quota while the participant was in progress. | | **4** | Survey Quality Term | Participant failed survey-level quality or consistency checks. | | **10** | System Quality Term | Internal system-level quality checks triggered a termination. | | **41** | Invalid Security Hash | The participant returned with an invalid or tampered security hash. | --- ## 4. Profile Completion For partners who rely on TapResearch to collect participant profile data instead of calling `/players/impanel`. | Code | Status | Description | | ------ | ---------------- | --------------------------------------------------------------------------------------- | | **61** | Profile Complete | The participant finished the TapResearch profile flow and is ready for survey matching. | --- # Support center Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/support-center # Support Center The `support_url` field returned on each campaign in the `/campaigns` response provides a TapResearch-hosted support contact form pre-loaded with session context for that participant. Use it to give players a direct path to TapResearch support without requiring you to handle survey-related issues yourself. --- ## When to show it Display a **"Get Help"** or **"Contact Support"** link alongside each campaign in your offerwall UI. The link should point to the `support_url` value for that specific campaign. Show it wherever the player can see the survey — on the survey list, on a detail card, or on a post-redirect screen. The goal is that if a player has a problem with a survey (e.g., completed but didn't receive credit), they have a clear path to report it. --- ## End-user flow 1. Player clicks your "Get Help" link. 2. TapResearch's support form opens, pre-loaded with the player's session context (campaign, publisher, session identifiers). 3. Player describes their issue and submits. 4. TapResearch support handles the inquiry directly. Publishers are not in the loop for individual support submissions — TapResearch owns resolution. --- ## Caveats ### No match or expired session The `support_url` encodes session state at the time the `/campaigns` response was generated. If TapResearch cannot match the session when the player opens the URL (e.g., the session has expired or the URL was cached), the support form will show an error directing the player back to the publisher's support team. **Publishers must provide a fallback contact channel.** Players who hit this error have no other path forward from TapResearch's side. ### Don't cache the URL `support_url` is session-specific. Fetch it fresh from the `/campaigns` response on each wall load — do not store and reuse it across sessions. ### Don't construct it manually Always use the value from the API response. The URL format is internal and may change without notice. --- # Best practices Source: https://supply-docs.tapresearch.com/docs/3.x/survey-feed-api/best-practices # Best Practices - **Refresh surveys every 2 minutes and whenever a user returns from a survey.** Survey availability changes frequently due to targeting, quotas, and real-time supply/demand. Regularly refreshing ensures users always see fresh, available surveys. Additionally, refresh immediately when a user returns from a survey — completing or exiting a survey can change which surveys they qualify for, so a fresh call ensures the list reflects their current eligibility. - **Keep participant profiles up to date.** Accurate and current profile data directly improves targeting and increases the likelihood of receiving surveys the user qualifies for. Each player's profile data is cleared after 90 days to ensure current data is used. - **Map qualifications carefully by country.** Each country has its own qualification set and `pre_code` values. Ensuring correct mapping improves match rates, reduces disqualification rates, and provides a better user experience. - **Use the `epc` field if you plan to manage your own yield.** Surveys are already ordered using TapResearch’s internal yield management algorithm. However, if you choose to override this ordering, rely on the `epc` field rather than `reward_amount`. Note that `reward_amount` reflects **only what the user is paid on a full complete** — TapResearch also pays out for DQs and OQs based on time spent in the survey. `epc` incorporates *all* payout types, giving you a more accurate signal of expected earnings per click if you're doing your own yield management. - **Remove surveys from the UI once a user clicks them.** Most surveys do **not** allow re-entry — if a user taps the same survey again, they’ll be immediately terminated. For surveys that *do* allow multiple entries, they will naturally reappear on the next refresh cycle. --- # Currencies Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/currencies # Currencies Currencies define how you reward your users. They are managed in the **Settings** tab of the TapResearch dashboard within your App Title. You can create multiple currencies, each with: - A unique name and icon - A configurable exchange rate ## Currency variants Each currency can have multiple **variants**, allowing you to offer different exchange rates to different user cohorts. ## Related Each currency also has an [exchange rate](./exchange-rate) and an optional [profile reward](./profile-reward). --- # Exchange Rates Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/exchange-rate # Exchange rates The **exchange rate** determines how much of your app currency a user receives for $1.00 USD earned. Each currency variant has its own rate. **Why it matters:** setting the right rate is crucial for a successful integration. Rates lower than 25 ($0.04 per unit of virtual currency) significantly reduce survey availability; higher rates unlock access to premium survey experiences. {children} ); ## Rate implications | Exchange rate | Value of 1 unit | Surveys available | | :-----------: | --------------: | :---------------: | | 1000 | $0.0010 | 100% | | 500 | $0.0020 | 100% | | 250 | $0.0040 | 100% | | 100 | $0.010 | 99% | | 80 | $0.0125 | 95% | | 50 | $0.020 | 85% | | 25 | $0.040 | 65% | | 16 | $0.0625 | 35% | | 10 | $0.10 | 15% | | 5 | $0.20 | 5% | :::tip Boost survey availability Running a [Currency Sale](../features/currency-sales) temporarily raises your exchange rate for a set period — a quick way to spike survey availability and engagement. ::: --- # Profile Reward Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/profile-reward # Profile reward The **profile reward** is a one-time bonus, paid in your currency, that a user earns the first time they **complete their profile** — the short set of demographic and screener questions that determine which surveys they qualify for. Profiling unlocks the survey wall, so the profile reward is a strong incentive to finish it. :::tip Recommended Setting a profile reward typically increases initial engagement and retention. We recommend **10–20% of your exchange rate**. It is a one-time reward for first-time users, and it is publisher-funded (not subsidized by TapResearch). ::: ## Setting it up Configure the profile reward on the **currency**, right next to the exchange rate, in **App → Settings → Currency**. You can set it per currency variant, so different cohorts can receive different profile rewards. Leave it at 0 for no profile reward. ## How it's paid - Paid **once** per user, the first time they complete profiling. - Paid in your virtual currency, in the amount you set — a currency amount, not a USD value run through the exchange rate. - Delivered through your normal reward callback, the same way as a survey completion. In the SDK's [TRReward](../basic-integration/sdk-integration/tr-reward), it arrives with `payoutEventType: profile_reward`. --- # Placements Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/placements # Placements A **placement** is a configured slot where TapResearch content appears, created in the **Supplier Dashboard**. You create placements no matter which integration you use, then reference each one by its **code tag**. The content-display mechanics on this page (`showContentForPlacement()`, content types) apply to the **SDK** integration. :::info If you make changes to placements, you'll need to reinitialize the SDK to see those changes. ::: There are **2 types of placements:** ### Pop-up Placements Use pop-up placements to display content automatically when the app loads, such as: * Sale banners * Interstitials * Quick Questions ### Survey Wall Placements Use survey wall placements to display content in response to user actions, such as: * Button clicks to open the survey wall * Earn center interactions Both placement types use the `showContentForPlacement()` call, but behave differently based on their type. :::info When a new placement is created, your dev team must implement it in your app code. Once the placement is created, you can add and remove content from it without needing to update your app. ::: ## Pop-up Placement Content Types Pop-up placements support the following content types: * **Interstitial** - Full-screen content shown between levels or at natural breaks * **Floating** - Smaller content displayed in the middle of the screen * **Banner** - Content that appears at the bottom of the screen ### Behavior When `showContentForPlacement()` is called: 1. The user sees the first piece of content matching their attributes 2. If no attributes are defined, the first content item is shown **Best practice:** Always include a default piece of content for each placement in case users don't match any specific attributes. See the example below: ## Survey Wall Placement Content Types Survey wall placements support: * **Survey wall** - Opens the full survey wall experience when triggered ## Creating a Placement 1. Click the **"Create placement"** button in the top right corner of the placements page 2. Fill out the placement details in the modal 3. Use the generated `Code tag` in your app to display the placement **Example:** `showContentForPlacement("earn-center")` ## Creating Content Once you have a placement: 1. Click the **"Create content"** button in the top right of the page 2. Select your content type from the modal 3. Fill out the form and click **"Submit"** Your content will immediately go live to users. :::tip Pro Tip To test specific content for QA, target yourself using user attributes. ::: --- # Reporting API Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/reporting-api ## Reporting API To get access to this stats API call, reach out to our account management team at [developers@tapresearch.com](mailto:developers@tapresearch.com). ### Get supplier stats ```ruby # Sample stats request "https://api.tapresearch.com/supply_api/suppliers/stats?access_token=83742aa7dc154c1a84d03213ac1228b5&from=2018-09-20T12:00:00-07:00&to=2018-09-20T13:00:00-07:00" ``` #### Sample Response ```json [ { "datetime": "2018-09-20T12:00:00-07:00", "app_name": "Sample App - iOS", "app_store_identifier": "com.sample.app", "placement_name": "Main Placement", "placement_identifier": "fb15ebe9c89c3fc4256c33a8e0eabcff", "country_abbr": "US", "country_name": "United States", "impressions": 10, "conversions": 5, "revenue": "1.32", "platform": "iOS" }, { "datetime": "2018-09-20T12:00:00-07:00", "app_name": "Sample App - iOS", "app_store_identifier": "com.sample.app", "placement_name": "Secondary Placement", "placement_identifier": "ab13eabbc89c3fc1956c83a8e0efbbfa", "country_abbr": "US", "country_name": "United States", "impressions": 1582, "conversions": 366, "revenue": "63.54", "platform": "iOS" }, { "datetime": "2018-09-20T12:00:00-07:00", "app_name": "Sample App - Android", "app_store_identifier": "com.sample.app", "placement_name": "Main Placement", "placement_identifier": "fb15eabbc89c3fc1466c83a8e0adbbcf", "country_abbr": "US", "country_name": "United States", "impressions": 45, "conversions": 19, "revenue": "3.56", "platform": "Android" } ] ``` ### HTTP Request `GET https://api.tapresearch.com/supply_api/suppliers/stats` ### Payload | Parameter | Type | Description | | ------------ | ------ | ------------------------------------------------------------------- | | access_token | String | Unique token used for authorization purposes. | | from | Date | The start date, in UTC date format: e.g. "2021-04-05T17:00:00-07:00 | | to | Date | The end date in UTC date format: e.g. "2021-04-12T17:00:00-011:30" | ### Response | Parameter | Type | Description | | -------------------- | ------- | ----------------------------------------------------------- | | datetime | date | The hour that the stats reflect, in UTC date format | | app_name | String | The App name | | app_store_identifier | String | App store identifier | | placement_name | String | The placement name | | placement_identifier | String | The unique placement identifier | | country_abbr | String | The country abbreviation | | country_name | String | The country name | | impressions | Integer | The number of entries to TapResearch experience | | conversions | Integer | The number of entries with at least one survey payout event | | revenue | Float | The revenue in the given hour | | platform | String | The app platform name | ## Daily stat roll ups Endpoint: `https://api.tapresearch.com/supply_api/suppliers/daily_stats` ### Payload | Parameter | Type | Description | | ------------ | ------ | ------------------------------------------------------------------- | | access_token | String | Unique token used for authorization purposes. | | from | Date | The start date, in UTC date format: e.g. "2021-04-05T17:00:00-07:00 | | to | Date | The end date in UTC date format: e.g. "2021-04-12T17:00:00-011:30" | ### Response | Parameter | Type | Description | | -------------------- | ------- | --------------------------------------------------- | | app_name | String | The App name | | app_store_identifier | String | App store identifier | | country_abbr | String | The country abbreviation | | country_name | String | The country name | | date | date | The date that the stats reflect, in UTC date format | | revenue | Float | The revenue in the given hour | | user_identifier | String | The user identifier | | platform | String | The app platform name | | impressions | Integer | The number of entries to TapResearch experience | ### Example response ```json [ { "app_name": "Sample App - iOS", "app_store_identifier": "com.sample.app", "country_abbr": "AU", "country_name": "Australia", "date": "2023-05-13", "revenue": "0.01", "user_identifier": "123456", "platform": "iOS", "impressions": 2 } ] ``` ## Quick Question Results Endpoint: `https://www.tapresearch.com/supply_api/suppliers/quick_question_results` ### Payload | Parameter | Type | Description | | ------------ | ------ | --------------------------------------------- | | access_token | String | Unique token used for authorization purposes. | ### Response | Parameter | Type | Description | | ----------- | ------- | -------------------------------------- | | survey_id | Integer | Unique survey ID | | survey_name | String | The Survey Name | | url | String | Url hosting quick question results csv | ### Example response ```json [ { "survey_id": "1", "survey_name": "Users favorite colors", "url": "url_path_to_hosting_response_data.com/results_csv" } ] ``` ## Player Summary To get access to this Player Summary API call, reach out to our account management team at [developers@tapresearch.com](mailto:developers@tapresearch.com). We will need publisher contact information for deletion requests. Endpoint: `https://www.tapresearch.com/supply_api/audience/players/summary.csv` ### Payload | Parameter | Type | Description | | ------------ | ------ | -------------------------------------------------------------------- | | access_token | String | Unique token used for authorization purposes. | | from | Date | The start date, in UTC date format: e.g. "2021-04-05T17:00:00-07:00" | | to | Date | The end date in UTC date format: e.g. "2021-04-12T17:00:00-011:30" | ### Response CSV containing rows for players that profiled between the from and to dates sent. --- # Testing Source: https://supply-docs.tapresearch.com/docs/3.x/testing-and-qa ## Testing All apps created in the [Publisher Dashboard](https://www.tapresearch.com/supplier_dashboard/overview) are set to **test mode** by default. **Test mode limitations:** * Survey wall is not available * Interactions are limited * Only test devices can access content **Purpose:** Verify the integration is properly configured before going live. :::info Applies to every integration Test mode, test devices, and going live apply to **all** integration paths. The verification walkthrough below uses the SDK survey wall as the example; with the Hosted Link, Eligibility API, or Survey Feed API, you verify through that path's own survey flow. ::: ### Steps to Go Live 1. [Add a test device](#add-a-test-device) 2. [Complete test mode verification](#test-mode) 3. [Promote the app to live mode](#live-mode) Once you set up a new app, select the **Testing** link in "App Overview" to navigate to the app checklist. ## Add a Test Device Only test devices can access apps in test mode. **To add a test device:** 1. Go to the [Publisher Dashboard](https://www.tapresearch.com/supplier_dashboard/devices) 2. Click **New Device** 3. Enter: * **Device test name** - A descriptive name for the device * **Device identifier** - Use one of: * iOS: [IDFA](https://developer.apple.com/documentation/adsupport/asidentifiermanager) * Android: [Advertising ID](http://www.androiddocs.com/google/play-services/id.html) * Or a unique user identifier
:::warning The identifier you send must be the one you register Test mode keys off the identifier your integration actually sends — there's no separate "test account." Whatever value you pass as the user identifier (the `userIdentifier` in the SDK, or the `uid` / `user_identifier` in a Hosted Link URL or API request) has to be **registered here as a test device**. If it isn't, that user sees **"no surveys available"** (SDK / Hosted Link) or gets **`has_offer: false` / `is_eligible: false`** (Eligibility API / Survey Feed API) — which looks like a broken integration but is really just test mode doing its job. Firing a test **callback/postback** on its own does **not** exercise this — a real session from a registered test device is what's required. ::: ## Test Mode Once the SDK is initialized, open the survey wall to enter test view. **Note:** If you're testing from an unsupported country, you'll default to the US and see US surveys. However, you may encounter issues taking surveys due to IP location restrictions. **Complete the test:** Click the **Complete** button to generate a test survey completion. This will trigger a callback. ## QA Checklist Once your test device is added: 1. Complete a test survey 2. Complete all other test tasks shown in the UI ## Live Mode **To go live:** 1. Navigate to **App Overview → Testing** 2. Click **Go Live** ### Testing After Going Live When your app is live but you're still using a test device: **If you haven't completed the prescreener:** **If you've completed the prescreener:** **Normal live mode (non-test device):** --- # Supported Countries Source: https://supply-docs.tapresearch.com/docs/3.x/operational-tools/supported-countries # Supported Countries and Languages TapResearch supports the following countries and language codes: | Country | Languages Supported | |-----------------|---------------------| | United States | EN, ES | | Canada | EN, FR | | Mexico | ES | | Brazil | PT | | Denmark | DE | | Finland | SV | | France | FR | | Germany | DE | | Italy | IT | | Norway | NB | | Spain | ES | | Sweden | SV | | United Kingdom | EN | | Poland | PL | | Netherlands | DA | | China | ZH-CN | | Hong Kong | ZH-TN | | India | EN, HI | | Japan | JA | | Singapore | EN | | South Korea | KO | | Philippines | EN | | Australia | EN | | New Zealand | EN | | South Africa | EN | --- # Callbacks with Reconciliation Source: https://supply-docs.tapresearch.com/docs/3.x/basic-integration/sdk-integration/callbacks-with-reconciliation ## Overview For an introduction to callbacks, see the [Callbacks documentation](./callbacks.md). This page covers two additional parameters for handling survey reconciliation: * **`status`** - Indicates if a completion was rejected * **`rejection_reason`** - Explains why a completion was rejected ## How Reconciliation Works * Rejections are sent either at completion time or in daily batches * A rejected payout arrives as a **second callback** with: * The same `tid` (transaction ID) * Status of **"Rejected"** * A rejection reason :::info Server-to-Server Only `status` and `rejection_reason` are **only available via server-to-server postback**. They are not sent through in-app callbacks. ::: ## 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, 10 - Bonus Bar Reward. | | 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. | | status | String | The status of the transaction. Possible values are **"Pending"** or **"Rejected"**. Except in extremely rare circumstances, **"Pending"** transactions will be updated to status **"Complete"** after 90 days. We will not fire the callback at the time but the payout is considered settled. **Only available for server-to-server postback.** | | rejection_reason | String | The reason for rejection, if applicable. **Only available for server-to-server postback.** | --- # FAQ Source: https://supply-docs.tapresearch.com/docs/3.x/faq #### My app hasn't gone live yet. Should I wait until it does before I integrate TapResearch? > We've had many successful apps integrate before going live. We encourage you to begin the integration process as early as possible. #### How can I be sure that my app is well-integrated with your SDK? > We encourage you to send us a test build so we can ensure proper integration between your app, our SDK, and our servers. #### What happened to other SDKs that were available in the past? > The following SDKs have been deprecated and are no longer available: - Adobe AIR - Javascript - Supplier API #### What is the difference in monetization between First Time Users (FTU) and Repeat Users? > All survey takers are valuable, but repeat users offer the largest ROI. > > The First Time User (FTU) experience is extremely important. We focus heavily on this experience to ensure users see the value of their time and the opportunity to earn rewards. Once an FTU converts to a repeat user, they quickly learn that successfully completing a 3–5 minute survey yields higher reward value than watching 5–10 videos. #### What is a user profile and how is it used to map users to commercial surveys? > User profiles are created when users first engage with the survey wall by answering profile questions. > > Surveys are matched to users based on their profile responses. As users complete more surveys, their LTV (lifetime value) increases, keeping them engaged within your app. #### When do we get paid? > Payments are processed approximately **90 days** after the month the revenue was earned. > > To ensure on-time payments, contact finance@tapresearch.com to request a payment form. #### What is the reconciliation process and why are user responses rejected? > Market researchers may reject user responses for various reasons. The main reasons include: > > * **VPN/Proxy usage** - Using VPNs or proxies > * **Poor quality responses** - Gibberish or nonsensical answers on open-ended questions > * **Speeding** - Completing surveys too quickly to have read the questions > * **Straightlining** - Selecting answers in a straight line on grid questions > * **Inconsistent responses** - Contradictory answers (e.g., claiming to never have heard of Netflix, then saying they pay for it) #### What if I have questions about billing or reconciliation? > Please submit any questions to [developers@tapresearch.com](mailto:developers@tapresearch.com). We will do our best to answer in a timely manner. #### What do we do if a user complains about an issue with a survey? > While rare, issues do occur. We empower users to contact us directly through the TapResearch experience, allowing them to report issues so we can reward the user and address any problems. > > **If you are contacted by a user:** > 1. Let us know immediately > 2. Email [developers@tapresearch.com](mailto:developers@tapresearch.com) with: > * User ID/Player ID/MAID > * Time and date of the issue > * Screenshots (if available) > > Our customer support team will respond to and begin investigating all reported incidents within **24 hours**. > > **Users can submit requests directly from the Survey Wall:** > > --- # Upgrading to 3.x from 2.x Source: https://supply-docs.tapresearch.com/docs/3.x/upgrading-to-3.x ## What Changed * **Banner interstitials and Quick Questions** - New content formats to engage users * **User attributes** - Send attributes in a hash map to target specific content to specific users * **Enhanced error handling** - New callbacks with more specific error codes * **Core SDK logic in JavaScript** - Enables us to push bug fixes and feature updates without requiring SDK updates * **Simplified placement management** - The SDK now manages placement state. Simply ask if content can be shown for a tag, receive a true/false response, and display the content accordingly ## Before You Begin ### Account Setup 1. **Enable your account** - Contact developers@tapresearch.com to enable your account for 3.x.x SDKs 2. **Create a new app** - Once enabled, create a new 3.x.x-enabled app (you'll need to recreate currencies and placements) 3. **Update credentials** - Update your app to use the new app's API token and placement tags ### Important Considerations * **⚠️ Breaking changes:** Version 3.x is not backwards compatible. Code changes are required (outlined below) * **Content requirement:** Add at least one active piece of content to your placements for `showContentForPlacement` and `canShowContentForPlacement` to return true * **Early initialization:** Initialize the SDK as early as possible once the user identifier is known. **Do NOT** use anonymous user identifiers, especially when using Quick Questions * **Placement strategy:** Set up placement calls early and throughout your app for survey walls, banners, interstitials, and Quick Questions. This prevents having to add them later ### Testing Setup Configure your test devices in the publisher dashboard: * **Android:** Use the [Android Advertising Identifier](https://developer.android.com/training/articles/ad-id) * **iOS:** Use the [IDFA](https://developer.apple.com/documentation/adsupport/asidentifiermanager) ### Learn More Not sure what Quick Questions is? Contact developers@tapresearch.com or see the Quick Questions section in this documentation. **Questions?** Don't hesitate to reach out to developers@tapresearch.com! ## Steps ### Update Dependencies Update your TapResearch dependency to **latest 3.6.x version** following the instructions for your platform: * [Android](basic-integration/sdk-integration/android) * [iOS](basic-integration/sdk-integration/ios) * [Unity](basic-integration/sdk-integration/unity) * [React Native](basic-integration/sdk-integration/react-native) ### Update Initialization Update your initialization code to include both the **API token** and the **user identifier** of the currently logged-in user. ### Update Callbacks **Reward Callback:** * Update to accept an **array of `TRReward` objects** instead of a single object * `TRReward` provides `rewardAmount` and `currencyName` properties * See [additional TRReward information](basic-integration/sdk-integration/tr-reward) **Error Callback:** * Create a global error callback to handle errors * **Android:** `TRError` includes `code` and `description` properties * **iOS:** `NSError` includes `code` and `localizedDescription` properties ### Update How You Call Placements The `showPlacement` method has been renamed: * **Most platforms:** `showContentForPlacement` * **iOS (Swift):** `showContent(forPlacement:)` ---