Everything you need to embed a survey, check its availability, and forward submissions. No SDK required — two endpoints and one URL.
The Flutter SDK (insightdive_sdk) is the recommended integration for mobile apps. It wraps the survey in a webview and streams lifecycle events. Two embedding modes are available:
The SDK manages the sheet — it opens, handles events, and auto-closes after submission. Minimal setup:
Insightdive.configure( tenant: 'acme', survey: 'onboarding', apiKey: 'ik_…', productVersion: '2026.5.1', scale: 1.0, // optional — content zoom, clamped [0.5, 2.0] context: { // optional — see Context fields ↓ 'plan': 'enterprise', 'trial_days_remaining': 12, 'beta_user': true, }, ); // anywhere a BuildContext is available: final result = await Insightdive.show(context); if (result.status == FeedbackStatus.completed) { // user submitted }
Drop InsightdiveSurvey anywhere in your widget tree — a full-screen page, a tab body, a card inside a list. You control when to show and remove it:
if (_showSurvey) InsightdiveSurvey( options: Insightdive.options, onEvent: (event) { if (event is FeedbackCompleted) { setState(() => _showSurvey = false); } }, ),
Events from the inline widget are not automatically forwarded to Insightdive.events. To do so, call Insightdive.addEvent(e) inside onEvent.
Auto-sizing: the modal bottom sheet fits the survey content height automatically. For the inline widget you control the size, so listen for FeedbackResize — its height is the content height in logical pixels — and size your container to match so the user never scrolls inside the survey:
onEvent: (event) {
if (event is FeedbackResize) {
setState(() => _surveyHeight = event.height);
}
},
Before mounting InsightdiveSurvey or calling show(), use isAvailable() to confirm the survey is live. This avoids showing a "not published" screen to end-users:
Insightdive.isAvailable().then((active) {
if (active) setState(() => _showSurvey = true);
});
Calls GET /api/v1/surveys/{survey}/status — public, no auth, cached 30 s. Returns false on any network error so your entry point stays hidden rather than crashing.
Install @insightdive/sdk from npm:
npm install @insightdive/sdk
Modal integration:
import { Insightdive } from '@insightdive/sdk';
Insightdive.configure({
tenant: 'acme',
survey: 'onboarding',
apiKey: 'ik_abc123...',
scale: 1, // optional — content zoom, clamped [0.5, 2]
context: { // optional — see Context fields ↓
plan: 'enterprise',
trialDaysRemaining: 12,
betaUser: true,
},
} satisfies InsightdiveOptions);
const available = await Insightdive.isAvailable();
if (available) {
const result = await Insightdive.show();
// result.status === 'completed' | 'dismissed'
}
Inline embed:
// container must have an explicit height set via CSS
const container = document.getElementById('survey-slot');
Insightdive.embed(container);
Lifecycle events:
Insightdive.on('completed', (e) => {
analytics.track('feedback_completed', { id: e.submissionId });
});
Insightdive.on('dismissed', () => {
analytics.track('feedback_dismissed');
});
The modal sheet auto-fits the survey content height (capped at 85dvh) — the survey page reports its height via a resize event the SDK consumes for you, so no manual sizing is needed.
Full API reference: Integrations page · npm listing.
The .NET SDK (Insightdive) is the recommended integration for Avalonia desktop applications. It presents the survey in an AvaloniaWebView inside a 460×680 dialog window — never full-screen, never modal-blocking. Two targets are shipped in the same package:
| Target | Notes |
|---|---|
net8.0 | Full Avalonia UI — modal dialog, inline control, optional screenshot capture |
netstandard2.0 | Status-check API only — no UI dependency, suitable for backends or non-Avalonia projects |
dotnet add package Insightdive
Call InsightdiveSDK.Configure() once at application startup, before any window is displayed:
InsightdiveSDK.Configure(new InsightdiveOptions { Tenant = "acme", Survey = "nps-q1", ApiKey = "ik_…", // Admin → Settings → API ProductVersion = Assembly.GetEntryAssembly() ?.GetName().Version?.ToString(), ProductIdentifier = "myapp-desktop", Locale = CultureInfo.CurrentCulture.Name, Theme = "dark", // "light" | "dark" Scale = 1.0, // content zoom, clamped [0.5, 2.0] — shrink/enlarge the survey WindowWidth = 480, // modal width (ShowAsync only) WindowHeight = 680, // modal height (ShowAsync only) AllowResize = false, // let the user resize the modal window Context = new Dictionary<string, object> // optional — see Context fields ↓ { ["plan"] = "enterprise", ["trial_days_remaining"] = 12, ["beta_user"] = true, }, });
Call IsAvailableAsync() before ShowAsync() to confirm the survey is active and the cooldown has elapsed. On network error, IsAvailableAsync fails open (returns true) so a transient blip never permanently hides the entry point:
if (await InsightdiveSDK.Instance.IsAvailableAsync()) { var result = await InsightdiveSDK.Instance.ShowAsync(this); if (result?.Status == FeedbackStatus.Completed) Console.WriteLine($"Submitted: {result.SubmissionId}"); }
Sizing & zoom. The modal opened by ShowAsync() uses WindowWidth/WindowHeight (default 480×680) and is non-resizable unless you set AllowResize = true. To make the survey content itself render smaller or larger inside the WebView — instead of being clipped — set Scale (CSS zoom, clamped to [0.5, 2.0]). Scale also applies to the inline InsightdiveSurveyControl, whose outer size is governed by your own layout.
Use TriggerAsync(eventName) to match a named SDK event against the survey's Trigger events list (configured in Admin → Insight → Settings → Delivery). If the event matches — or the list is empty — the method returns true:
// Fires only when "onboarding_completed" matches the survey's trigger list if (await InsightdiveSDK.Instance.TriggerAsync("onboarding_completed")) await InsightdiveSDK.Instance.ShowAsync(this);
Subscribe to FeedbackEventOccurred to react to every step of the survey session:
InsightdiveSDK.Instance.FeedbackEventOccurred += (_, e) =>
{
switch (e)
{
case FeedbackViewedEvent v:
analytics.Track("survey_viewed", v.SessionId); break;
case FeedbackCompletedEvent c:
analytics.Track("survey_completed", c.SubmissionId); break;
case FeedbackDismissedEvent d:
analytics.Track("survey_dismissed"); break;
}
};
The ShowAsync window auto-fits its height to the survey content (unless AllowResize is enabled). A FeedbackResizeEvent (with a Height in logical pixels) is also raised on every layout change if you embed the inline control and want to size it yourself.
Embed the survey directly in your layout instead of a modal. Use InsightdiveSurveyControl from the Insightdive.Avalonia_ namespace:
using Insightdive.Avalonia_; var opts = InsightdiveSDK.Instance.Options; var token = await InsightdiveSDK.Instance.FetchEmbedTokenAsync(); var url = UrlBuilder.SurveyUrl(opts, token); var control = new InsightdiveSurveyControl(InsightdiveSDK.Instance, url); control.EventOccurred += (_, e) => { /* handle events */ }; MyContentPanel.Children.Add(control);
The netstandard2.0 target exposes the availability API with no UI dependency — useful for server-side code or non-Avalonia apps that only need to know whether a survey is live:
InsightdiveSDK.Configure(new InsightdiveOptions { Tenant = "acme", Survey = "nps-q1", ApiKey = "ik_…" }); bool available = await InsightdiveSDK.Instance.IsAvailableAsync(); bool triggered = await InsightdiveSDK.Instance.TriggerAsync("app_launched");
Full options reference: Integrations page · NuGet listing.
All three SDKs accept an optional context object at configure() time. Context is forwarded to every submission and stored as opaque JSON alongside the answers. Admins can view context values in the response detail, filter responses by context keys, and export them as context.* columns in CSV exports.
Anonymous to us — Insightdive stores the context blob verbatim without interpreting the keys. Never put user IDs, emails, or other PII in context fields. If you need to correlate responses with your users, use the Respondent Token pattern instead.
| Constraint | Limit |
|---|---|
| Max keys | 20 |
| Key format | [a-z0-9_], max 64 chars |
| Value types | string (max 255 chars), number, or boolean |
| Total JSON size | 4 KB |
Invalid keys or values are silently dropped — the submission is never rejected because of bad context metadata.
// Segment by subscription plan context: { plan: 'enterprise', trial_days_remaining: 12 } // Feature flag context context: { feature_vault_v2: true, feature_export: false } // Surface / flow context context: { surface: 'onboarding', step: 'invite_team' }
A respondent token is an opaque string your system generates and passes to the SDK at configure() time. Insightdive stores it verbatim alongside the submission — it is never decoded, never exposed to respondents, and never used to identify anyone. It gives your team a way to cross-reference a response with your own internal records without sending a PII identifier to Insightdive.
The recommended approach is to hash the user ID together with a server-side secret (workspace salt) so the token is opaque to Insightdive while still being computable from your side whenever you need to look up all responses from a user:
import { createHmac } from 'node:crypto'; // WORKSPACE_SALT is a per-workspace secret stored server-side — never exposed to the client. const token = createHmac('sha256', process.env.WORKSPACE_SALT) .update(userId) .digest('hex'); Insightdive.configure({ tenant: 'acme', survey: 'nps-q1', apiKey: '...', respondentToken: token, // opaque to Insightdive, computable by you });
import 'dart:convert'; import 'package:crypto/crypto.dart'; final key = utf8.encode(workspaceSalt); // server-provided, never embedded in the app final bytes = utf8.encode(userId); final token = Hmac(sha256, key).convert(bytes).toString(); Insightdive.configure( tenant: 'acme', survey: 'nps-q1', apiKey: '...', respondentToken: token, );
using System.Security.Cryptography; using System.Text; var keyBytes = Encoding.UTF8.GetBytes(WorkspaceSalt); // server-provided secret var msgBytes = Encoding.UTF8.GetBytes(userId); using var hmac = new HMACSHA256(keyBytes); var token = Convert.ToHexString(hmac.ComputeHash(msgBytes)).ToLower(); InsightdiveSDK.Configure(new InsightdiveOptions { Tenant = "acme", Survey = "nps-q1", ApiKey = "...", RespondentToken = token, });
| Constraint | Limit |
|---|---|
| Max length | 128 characters |
| Allowed characters | [a-zA-Z0-9-_] only |
| Indexed | Yes — exact-match filter in the admin and API |
Tokens that fail validation are silently dropped — the submission is never rejected because of an invalid token.
a3f2c1d8…e9b1. It cannot reverse this to a user ID.The contact opt-in is a user-initiated, operator-forwarded feature. When enabled, the survey editor lets you add a contact_opt_in question. The respondent sees a checkbox ("I'm open to a follow-up") and an optional input field. If they consent, Insightdive:
Anonymous to us: the contact value is never shown in the Insightdive admin UI in plaintext. The admin only shows consent status, field name, and webhook delivery status. Insightdive acts as a transient relay, not a contact book.
Contact opt-in is disabled by default. Enable it in your workspace under Settings → Contact Opt-in. Once enabled, a new contact_opt_in question type appears in the survey editor.
In the survey editor, add a contact_opt_in question. Configure:
email | phone | ticket_refThe contact opt-in question is always optional for the respondent (they can uncheck and submit without providing any contact info).
When a submission includes an opt-in answer, the submission.created webhook payload includes a contactOptIn field:
{
"event": "submission.created",
"data": {
"submissionId": "clxxx...",
"projectName": "Onboarding NPS",
"receivedAt": "2026-05-15T14:32:00.000Z",
"preview": "How likely are you to recommend → 9",
"contactOptIn": {
"consented": true,
"fieldName": "email",
"value": "user@example.com"
}
}
}
When the respondent did not consent, consented is false and value is null. When no contact opt-in question was in the survey, contactOptIn is null.
| What | Insightdive stores | TTL |
|---|---|---|
| Consent status | Yes (always) | With the submission (no TTL) |
| Field name | Yes (no PII) | With the submission (no TTL) |
| Contact value | AES-256-GCM encrypted | 7 days (deleted after webhook delivery) |
| Plaintext value | Never stored | — |
If your webhook endpoint is unreachable for more than 7 days, the encrypted data is purged and the contact value is permanently lost. Ensure your webhook is healthy.
Insightdive is URL-based: the survey lives at a public URL that you open in a small popup. The embedding app only needs to do two things:
GET /api/v1/surveys/{slug}/status. If enabled is false, hide the button silently — the user never sees an error page./s/{slug} with required query params in a small popup (bottom sheet on mobile, ~460×680 window on desktop). All survey logic — questions, AI follow-ups, submission — runs inside that page.No SDK is required on the client side. Submissions are anonymous: only productIdentifier, productVersion, and the user's answers are collected. No user ID, no email, no IP address.
The status endpoint is public — no credentials needed. All endpoints that read or write private data require a workspace API key sent as a bearer token:
Authorization: Bearer <your-api-key>
Find your API key in your workspace under Settings → API. It is scoped to your tenant — never expose it in client-side code.
Checks whether a survey is live and ready to display. Always returns 200 — branch on the enabled boolean only. Any network error or non-200 response should be treated as enabled: false so the entry point stays hidden.
{
"slug": "my-project",
"enabled": true,
"name": "My Project",
"url": "https://<tenant>.insightdive.com/s/my-project",
"acceptsLocale": true, // pass ?locale= if true
"acceptsTheme": true // pass ?theme= if true
}
// reason: "disabled" | "not_published" | "not_found" { "slug": "my-project", "enabled": false, "reason": "disabled" }
When enabled is true, open this URL in a subtle popup — never full-screen. On mobile, use a bottom sheet at ~75% height. On desktop, a window.open at 460×680 px works well.
https://<tenant>.insightdive.com/s/my-project ?productIdentifier=myapp-desktop # required &productVersion=2026.5.1 # required &locale=en # optional — only if acceptsLocale: true &theme=dark # optional — only if acceptsTheme: true
| Parameter | Description |
|---|---|
| productIdentifierrequired | Identifies which embedding product is sourcing the feedback (e.g. myapp-desktop, myapp-mobile). Lets you filter responses by surface in the admin when several products share the same survey. |
| productVersionrequired | The build of the embedding product (e.g. 2026.5.1). Stored with each response for version-level filtering. |
| localeoptional | Language code to pass to the survey (e.g. fr, en). Honored only when acceptsLocale is true in the status response. |
| themeoptional | Visual theme: light or dark. Honored only when acceptsTheme is true in the status response. |
Creates a submission directly. Use this only for server-to-server integrations — backends that already collect feedback and want to forward it to Insightdive. End-user clients should use the /s/{slug} URL instead, which handles the survey UI and submission automatically.
{
"schemaVersion": 1, // optional, currently 1
"projectId": "<project-id>", // required — find in Admin → Project
"productIdentifier": "myapp-desktop", // recommended
"productVersion": "2026.5.1", // recommended
"context": { // optional — see Context fields ↑
"plan": "enterprise",
"trial_days_remaining": 12
},
"respondentToken": "a3f2...e9b1", // optional — see Respondent token ↑
"summary": "Loved the new vault picker.",
"transcript": [
{
"questionId": "q1",
"questionText": "How would you rate it?",
"type": "number",
"value": "9"
}
]
}
{
"id": "clxxxxxxxxxxxxxxxxxxxxxxx",
"receivedAt": "2026-05-15T14:32:00.000Z",
"status": "new"
}
Returns submissions for the authenticated workspace, newest first. Capped at 100 per call.
| Query param | Description |
|---|---|
| projectIdoptional | Filter to a specific project by ID. |
| statusoptional | Filter by status: new | reviewed | archived |
| limitoptional | Max results (1–100, default 20). |
{
"count": 2,
"submissions": [
{
"id": "clxxxxxxxxxxxxxxxxxxxxxxx",
"receivedAt": "2026-05-15T14:32:00.000Z",
"status": "new",
"productIdentifier": "myapp-desktop",
"productVersion": "2026.5.1",
"summary": "Loved the new vault picker.",
"project": { "id": "...", "name": "My Project", "slug": "my-project" }
}
]
}
All endpoints return structured JSON errors. Rate limits apply per IP address.
| Status | Error | Meaning |
|---|---|---|
| 400 | Invalid body |
Request body failed schema validation. Check the details field for the exact path. |
| 401 | Unauthorized |
Missing or invalid bearer token. |
| 402 | plan_limit_reached |
Workspace has hit its monthly submission limit. Upgrade or wait for the next cycle. |
| 403 | workspace_suspended |
Workspace is suspended. Contact support. |
| 404 | Project not found |
The projectId does not exist in this workspace. |
| 429 | rate_limited |
Too many requests. Check the Retry-After header (seconds). Limits: 120 req/min on /status, 60 req/min on /submissions. |
The integration guide in your admin generates code snippets against your own project slug and tenant URL.
Get your workspace