For developers

API reference.

Everything you need to embed a survey, check its availability, and forward submissions. No SDK required — two endpoints and one URL.

Flutter SDK

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:

Modal bottom sheet

The SDK manages the sheet — it opens, handles events, and auto-closes after submission. Minimal setup:

main.dart
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 }

Inline widget

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:

my_page.dart
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:

my_page.dart
onEvent: (event) {
  if (event is FeedbackResize) {
    setState(() => _surveyHeight = event.height);
  }
},

Checking availability

Before mounting InsightdiveSurvey or calling show(), use isAvailable() to confirm the survey is live. This avoids showing a "not published" screen to end-users:

initState
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.

Web / JavaScript SDK

Install @insightdive/sdk from npm:

Terminal
npm install @insightdive/sdk

Modal integration:

TypeScript / JavaScript
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:

TypeScript / JavaScript
// container must have an explicit height set via CSS
const container = document.getElementById('survey-slot');
Insightdive.embed(container);

Lifecycle events:

TypeScript / JavaScript
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.

.NET / C# SDK

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:

TargetNotes
net8.0Full Avalonia UI — modal dialog, inline control, optional screenshot capture
netstandard2.0Status-check API only — no UI dependency, suitable for backends or non-Avalonia projects

Install

Terminal
dotnet add package Insightdive

Configure

Call InsightdiveSDK.Configure() once at application startup, before any window is displayed:

App.axaml.cs
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,
    },
});

Show as a modal dialog

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:

MainWindow.axaml.cs
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.

Event-based triggering

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:

C#
// Fires only when "onboarding_completed" matches the survey's trigger list
if (await InsightdiveSDK.Instance.TriggerAsync("onboarding_completed"))
    await InsightdiveSDK.Instance.ShowAsync(this);

Lifecycle events

Subscribe to FeedbackEventOccurred to react to every step of the survey session:

C#
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.

Inline control

Embed the survey directly in your layout instead of a modal. Use InsightdiveSurveyControl from the Insightdive.Avalonia_ namespace:

C# — net8.0 only
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);

Pure .NET — status check only

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:

C# — netstandard2.0
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.

Context fields

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.

Constraints

ConstraintLimit
Max keys20
Key format[a-z0-9_], max 64 chars
Value typesstring (max 255 chars), number, or boolean
Total JSON size4 KB

Invalid keys or values are silently dropped — the submission is never rejected because of bad context metadata.

Example use cases

Context examples
// 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' }

Respondent token

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.

Pattern: hash instead of ID

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:

Token derivation — TypeScript / Node.js
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
});
Token derivation — Dart / Flutter
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,
);
Token derivation — C# / .NET
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,
});

Constraints

ConstraintLimit
Max length128 characters
Allowed characters[a-zA-Z0-9-_] only
IndexedYes — 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.

What Insightdive sees vs. what you can do

Contact opt-in (Tier 3)

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:

  1. Encrypts the contact value (AES-256-GCM) before storing it.
  2. Includes the plaintext value in the webhook payload so your system receives it immediately.
  3. Purges the encrypted copy from Insightdive's storage after 7 days (or sooner, once webhook delivery is confirmed).

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.

Enabling the feature

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.

Question configuration

In the survey editor, add a contact_opt_in question. Configure:

The contact opt-in question is always optional for the respondent (they can uncheck and submit without providing any contact info).

Webhook payload

When a submission includes an opt-in answer, the submission.created webhook payload includes a contactOptIn field:

Webhook payload — submission.created (with opt-in)
{
  "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.

Data retention

WhatInsightdive storesTTL
Consent statusYes (always)With the submission (no TTL)
Field nameYes (no PII)With the submission (no TTL)
Contact valueAES-256-GCM encrypted7 days (deleted after webhook delivery)
Plaintext valueNever 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.

Architecture

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:

  1. Check availability. Before showing the "Give feedback" button, call GET /api/v1/surveys/{slug}/status. If enabled is false, hide the button silently — the user never sees an error page.
  2. Open the survey. If active, open /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.

Authentication

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:

HTTP header
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.

Survey status

GET /api/v1/surveys/{slug}/status Public · no auth CORS open Cached 30 s

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.

Response — enabled

200 OK
{
  "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
}

Response — disabled

200 OK (survey not available)
// reason: "disabled" | "not_published" | "not_found"
{ "slug": "my-project", "enabled": false, "reason": "disabled" }

Survey URL

URL /s/{slug}

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.

Full URL format
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.

Submit a response

POST /api/v1/submissions Bearer token Server-to-server

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.

Request body

POST body (application/json)
{
  "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"
    }
  ]
}

Response

201 Created
{
  "id":         "clxxxxxxxxxxxxxxxxxxxxxxx",
  "receivedAt": "2026-05-15T14:32:00.000Z",
  "status":     "new"
}

List responses

GET /api/v1/submissions Bearer token

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).
200 OK
{
  "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" }
    }
  ]
}

Errors & rate limits

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.

Ready to embed?

The integration guide in your admin generates code snippets against your own project slug and tenant URL.

Get your workspace