Callbacks
The SDK reports what happens to a survey through six callbacks. All of them are optional and all are delivered on the main (UI) thread.
| Callback | Fires when | Fields |
|---|---|---|
surveyShown | The survey became visible. | survey (id, name), trigger (type, name) |
surveyNotShown | A survey matched a trigger, or was requested with showSurvey, but was not shown. | surveyId, reason |
questionAnswered | The respondent answered a question. | surveyId, questionId, type, title, value |
surveyCompleted | The respondent submitted the survey. | surveyId, responseId, answers |
surveyDismissed | The survey was closed before it was completed. | surveyId |
surveyError | The survey reported an error while it was visible. | surveyId, code, message |
Register a listener
Section titled “Register a listener”Implement ResponslyListener. Every method has a default implementation, so write only the ones you need.
import ResponslySDK
final class SurveyEvents: ResponslyListener { func surveyShown(_ event: ResponslySurveyShownEvent) { print("shown", event.survey.id, event.survey.name, event.trigger.type.rawValue) }
func surveyNotShown(_ event: ResponslySurveyNotShownEvent) { print("not shown", event.surveyId, event.reason.rawValue) }
func questionAnswered(_ event: ResponslyQuestionAnsweredEvent) { print("answered", event.questionId, event.type, event.title, event.value) }
func surveyCompleted(_ event: ResponslySurveyCompletedEvent) { print("completed", event.responseId, event.answers.count) }
func surveyDismissed(_ event: ResponslySurveyDismissedEvent) { print("dismissed", event.surveyId) }
func surveyError(_ event: ResponslySurveyErrorEvent) { print("error", event.code, event.message) }}
let token = Responsly.shared.addListener(SurveyEvents())
// laterResponsly.shared.removeListener(token)The SDK keeps the listener until you remove it with the token.
ResponslyListenerBlock is the closure-based variant. Pass only the closures you need:
let token = Responsly.shared.addListener(ResponslyListenerBlock( surveyCompleted: { event in print("completed", event.responseId) }, surveyDismissed: { event in print("dismissed", event.surveyId) }))reason is a ResponslyNotShownReason and trigger.type is a ResponslyTriggerType. Their rawValue is the string used throughout these docs, such as frequency_limit or app_launch. Every event has toDictionary(), which returns the fields with snake_case keys.
Implement ResponslyListener. Every method has a default implementation, so override only the ones you need.
import com.responsly.sdk.*
val listener = object : ResponslyListener { override fun onSurveyShown(event: SurveyShownEvent) { Log.d("Survey", "shown ${event.survey.id} ${event.survey.name} by ${event.trigger.type}") }
override fun onSurveyNotShown(event: SurveyNotShownEvent) { Log.d("Survey", "not shown ${event.surveyId}: ${event.reason.value}") }
override fun onQuestionAnswered(event: QuestionAnsweredEvent) { Log.d("Survey", "answered ${event.questionId} (${event.type}): ${event.value}") }
override fun onSurveyCompleted(event: SurveyCompletedEvent) { Log.d("Survey", "completed ${event.responseId}, ${event.answers.size} answers") }
override fun onSurveyDismissed(event: SurveyDismissedEvent) { Log.d("Survey", "dismissed ${event.surveyId}") }
override fun onSurveyError(event: SurveyErrorEvent) { Log.w("Survey", "error ${event.code}: ${event.message}") }}
Responsly.addListener(listener)
// laterResponsly.removeListener(listener)Every event has toMap(), which returns the fields with snake_case keys.
Pass an object with the handlers you need. addListener returns a function that removes the listener.
const unsubscribe = Responsly.addListener({ onSurveyShown: ({ survey, trigger }) => console.log('shown', survey.id, survey.name, trigger.type), onSurveyNotShown: ({ surveyId, reason }) => console.log('not shown', surveyId, reason), onQuestionAnswered: ({ surveyId, questionId, type, title, value }) => console.log('answered', questionId, type, value), onSurveyCompleted: ({ surveyId, responseId, answers }) => console.log('completed', responseId, answers.length), onSurveyDismissed: ({ surveyId }) => console.log('dismissed', surveyId), onSurveyError: ({ surveyId, code, message }) => console.warn('error', code, message),})
// laterunsubscribe()In a component, return it from an effect:
useEffect(() => Responsly.addListener({ onSurveyCompleted: ({ responseId }) => console.log('completed', responseId),}), [])Payload keys are camelCase in JavaScript.
Responsly.events is a broadcast Stream<ResponslyEvent>. ResponslyEvent is a sealed class, so a switch over it is checked for completeness.
final subscription = Responsly.events.listen((event) { switch (event) { case SurveyShown(:final survey, :final trigger): debugPrint('shown ${survey.id} ${survey.name} by ${trigger.type}'); case SurveyNotShown(:final surveyId, :final reason): debugPrint('not shown $surveyId: $reason'); case QuestionAnswered(:final questionId, :final type, :final value): debugPrint('answered $questionId ($type): $value'); case SurveyCompleted(:final responseId, :final answers): debugPrint('completed $responseId, ${answers.length} answers'); case SurveyDismissed(:final surveyId): debugPrint('dismissed $surveyId'); case SurveyError(:final code, :final message): debugPrint('error $code: $message'); }});
// latersubscription.cancel();To handle one event only:
Responsly.events.listen((event) { if (event case SurveyCompleted(:final responseId)) { debugPrint('completed $responseId'); }});The callbacks are static C# events on ResponslySDK, raised on Unity’s main thread.
ResponslySDK.SurveyShown += e => Debug.Log($"shown {e.Survey.Id} {e.Survey.Name} by {e.Trigger.Type}");ResponslySDK.SurveyNotShown += e => Debug.Log($"not shown {e.SurveyId}: {e.Reason}");ResponslySDK.QuestionAnswered += e => Debug.Log($"answered {e.QuestionId} ({e.Type}): {e.ValueJson}");ResponslySDK.SurveyCompleted += e => Debug.Log($"completed {e.ResponseId}");ResponslySDK.SurveyDismissed += e => Debug.Log($"dismissed {e.SurveyId}");ResponslySDK.SurveyError += e => Debug.LogWarning($"error {e.Code}: {e.Message}");Subscribe with a named method if you need to unsubscribe with -= later.
You can register several listeners. Each receives every event.
Order of events
Section titled “Order of events”For one survey the callbacks arrive in this order:
surveyShown, once.questionAnswered, zero or more times.- Either
surveyCompletedorsurveyDismissed, once.
A survey that does not get on screen produces a single surveyNotShown instead. surveyError can arrive at any point between 1 and 3.
One exception: when closeSurvey cancels a survey that was still waiting to appear, surveyDismissed fires without a surveyShown before it. Write handlers that resume your app so that they also work when nothing was paused.
There is no callback for the moment a completed survey leaves the screen. After surveyCompleted the thank-you page stays until the auto-close time passes or the respondent closes it. If your code must continue only when the screen is clear, call closeSurvey in the surveyCompleted handler.
Events in detail
Section titled “Events in detail”surveyShown
Section titled “surveyShown”| Field | Description |
|---|---|
survey.id | The Survey ID. |
survey.name | The form’s name in Responsly. |
trigger.type | event, screen, app_launch, or manual for showSurvey. |
trigger.name | The event or screen name. null (absent on iOS) for app_launch and manual. |
Fires when the survey is actually visible, after it has loaded. This is the moment that counts as a display for frequency rules and for the shown count in the Mobile app tab. Pause video, audio or gameplay here.
surveyNotShown
Section titled “surveyNotShown”| Field | Description |
|---|---|
surveyId | The Survey ID. |
reason | One of the not-shown reasons, for example frequency_limit or audience_mismatch. |
Fires for every survey that matched a trigger or was requested with showSurvey and then failed a check. It is the programmatic twin of the debug log. Expect it often: with several active surveys, most trigger calls end here for most of them.
questionAnswered
Section titled “questionAnswered”| Field | Description |
|---|---|
surveyId | The Survey ID. |
questionId | The question’s ID. |
type | The question’s type, for example nps, rating, multiple_choice, text. |
title | The question text as shown to the respondent. |
value | The answer. See Answer values. |
Fires while the respondent is filling in the survey, before it is submitted. If the survey is dismissed afterwards, you have still received these events.
surveyCompleted
Section titled “surveyCompleted”| Field | Description |
|---|---|
surveyId | The Survey ID. |
responseId | The ID of the saved response. |
answers | A list of answers, each with questionId, type, title, value. |
Fires when the respondent submits the survey. From this moment the survey counts as completed for frequency rules.
surveyDismissed
Section titled “surveyDismissed”| Field | Description |
|---|---|
surveyId | The Survey ID. |
Fires when the survey goes away before completion: the respondent tapped the close button or the backdrop, pressed back on Android, or your code called closeSurvey. It does not fire when a completed survey closes.
surveyError
Section titled “surveyError”| Field | Description |
|---|---|
surveyId | The Survey ID. |
code | A short error code. |
message | A description for logs. Do not show it to users. |
Fires when something goes wrong in a survey that is already on screen. Problems before that point are reported as surveyNotShown with load_failed or survey_unavailable.
Answer values
Section titled “Answer values”value is the answer as the survey stores it: a string, a number, a boolean, a list or a map, depending on the question type. Number scales such as NPS give a number.
| Platform | How value arrives |
|---|---|
| iOS | A JSON-compatible value: String, NSNumber, array or dictionary. Cast it: (value as? NSNumber)?.intValue. |
| Android | A JSON-compatible value: String, Number, Boolean, List or Map. Cast it: (value as? Number)?.toInt(). |
| React Native | A plain JavaScript value. |
| Flutter | A plain Dart value: String, num, bool, List or Map. |
| Unity | JSON text in ValueJson, with the helpers AsString(), AsDouble() and AsBool(). |
Find a question by questionId when you need one specific question, or by type when the survey has a single question of that type.
Recipe: ask for a store review after NPS 9 or 10
Section titled “Recipe: ask for a store review after NPS 9 or 10”Listen for surveyCompleted, find the NPS answer, and start the platform’s review flow for promoters.
surveyCompleted fires while the thank-you page is still visible. Close the survey first, then ask.
import StoreKitimport UIKitimport ResponslySDK
final class ReviewPrompt: ResponslyListener { func surveyCompleted(_ event: ResponslySurveyCompletedEvent) { guard let nps = event.answers.first(where: { $0.type == "opinion_scale" }), let score = (nps.value as? NSNumber)?.intValue, score >= 9 else { return }
Responsly.shared.closeSurvey()
// Give the survey a moment to leave the screen. DispatchQueue.main.asyncAfter(deadline: .now() + 1) { guard let scene = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .first(where: { $0.activationState == .foregroundActive }) else { return }
if #available(iOS 16.0, *) { AppStore.requestReview(in: scene) } else { SKStoreReviewController.requestReview(in: scene) } } }}
// At app start, after initialize:let reviewPromptToken = Responsly.shared.addListener(ReviewPrompt())Add the Play In-App Review library (com.google.android.play:review-ktx).
import android.app.Activityimport com.google.android.play.core.review.ReviewManagerFactoryimport com.responsly.sdk.*
class ReviewPrompt(private val activity: Activity) : ResponslyListener { override fun onSurveyCompleted(event: SurveyCompletedEvent) { val nps = event.answers.firstOrNull { it.type == "opinion_scale" } ?: return val score = (nps.value as? Number)?.toInt() ?: return if (score < 9) return
Responsly.closeSurvey()
val manager = ReviewManagerFactory.create(activity) manager.requestReviewFlow().addOnCompleteListener { request -> if (request.isSuccessful) { manager.launchReviewFlow(activity, request.result) } } }}Register it for the lifetime of the activity:
class MainActivity : AppCompatActivity() { private lateinit var reviewPrompt: ReviewPrompt
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) reviewPrompt = ReviewPrompt(this) Responsly.addListener(reviewPrompt) }
override fun onDestroy() { Responsly.removeListener(reviewPrompt) super.onDestroy() }}This example uses the community package react-native-in-app-review. In Expo, expo-store-review (StoreReview.requestReview()) does the same job.
import InAppReview from 'react-native-in-app-review'import Responsly from '@responsly/react-native-sdk'
const unsubscribe = Responsly.addListener({ onSurveyCompleted: ({ answers }) => { const nps = answers.find((answer) => answer.type === 'opinion_scale') if (typeof nps?.value !== 'number' || nps.value < 9) return
Responsly.closeSurvey()
// Give the survey a moment to leave the screen. setTimeout(() => { if (InAppReview.isAvailable()) { InAppReview.RequestInAppReview() } }, 1000) },})This example uses the community package in_app_review.
import 'package:in_app_review/in_app_review.dart';import 'package:responsly_sdk/responsly_sdk.dart';
final subscription = Responsly.events.listen((event) async { if (event case SurveyCompleted(:final answers)) { final score = answers .where((answer) => answer.type == 'opinion_scale') .map((answer) => answer.value) .whereType<num>() .firstOrNull; if (score == null || score < 9) return;
Responsly.closeSurvey();
// Give the survey a moment to leave the screen. await Future<void>.delayed(const Duration(seconds: 1));
final review = InAppReview.instance; if (await review.isAvailable()) { await review.requestReview(); } }});Remember the NPS answer in QuestionAnswered and act on it in SurveyCompleted.
using System.Globalization;using Responsly;using UnityEngine;
public static class ReviewPrompt{ private static double? npsScore;
[RuntimeInitializeOnLoadMethod] private static void Init() { ResponslySDK.SurveyShown += e => { npsScore = null; };
ResponslySDK.QuestionAnswered += e => { if (e.Type == "opinion_scale" && double.TryParse(e.ValueJson, NumberStyles.Float, CultureInfo.InvariantCulture, out var score)) { npsScore = score; } };
ResponslySDK.SurveyCompleted += e => { if (npsScore == null || npsScore < 9) return;
ResponslySDK.CloseSurvey();#if UNITY_IOS UnityEngine.iOS.Device.RequestStoreReview();#elif UNITY_ANDROID // Start the review flow of the Google Play In-App Review plugin (com.google.play.review).#endif }; }}Next steps
Section titled “Next steps”- Send survey events to your analytics tool: Integrations.
- Find out why a survey did not appear: Testing & debugging.