Survey Widget

Render multi-question, client-triggered surveys with banner, modal, or inline display and per-survey trigger rules

Overview

The Survey widget renders multi-question surveys inside your product — open text, scale, NPS, single/multi choice, boolean — and only fires when one of the survey's triggers matches (URL pattern, custom event, or time-on-page). The widget pulls live survey definitions from the server on load, so customers paste the snippet once and you author surveys from the dashboard.

This is the in-product counterpart to the email magic-link survey landing page. Surveys triggered by lifecycle events (trial expiry, subscription cancelled, etc.) reach users via email; this widget covers the client-side trigger path.

Three display modes are supported:

  • Banner — bottom slide-in (default)
  • Modal — centered overlay with backdrop
  • Inline — mounted inside a container you specify

Quick Start

Drop the script anywhere on your page:

html
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"></script>

The widget fetches live surveys from GET /api/v1/projects/{project_key}/surveys-config. It only fires when all of these hold:

  1. A survey is in Live status with a ClientSide trigger.
  2. A trigger rule matches (URL pattern, custom event, or time-on-page).
  3. The visitor is past the survey's cooldown window (default 90 days).
  4. The per-survey sample-rate roll passes (default 100%).
  5. The org is on the Pro plan.

Configuration Options

Required Attributes

data-project-keystringrequired

Your unique project identifier from the SeggWat dashboard.

Optional Attributes

data-modestringdefault:"banner"

Display mode. Valid values: banner, modal, inline.

  • banner — fixed bottom slide-in, dismissible
  • modal — centered overlay with backdrop
  • inline — mounted into data-container, no backdrop
html
data-mode="modal"
data-containerstring

CSS selector for the container to mount the widget into. Required when data-mode="inline".

html
data-container="#survey-slot"
data-survey-idstring

When set, only the survey with this id is considered and its triggers are bypassed — the widget shows it as soon as it loads (still subject to cooldown / sample rate). Useful for embedding a specific survey on a static landing page.

data-cooldown-daysnumber

Override the per-survey cooldown for all surveys returned by this embed. 0 disables the cooldown entirely. Intended for dev / QA.

data-button-colorstringdefault:"#2563eb"

Hex color used for primary buttons, selected states, and progress bar. Hex format only (e.g. #10b981).

data-languagestringdefault:"auto-detect"

Language code for the widget UI chrome (Step labels, Next/Back/Submit/Skip). Supported: en, de, sv, fr. Falls back to English when unsupported. Per-survey copy overrides authored in the dashboard take precedence over these defaults.

data-show-powered-bybooleandefault:"true"

Show or hide the "Powered by SeggWat" footer. Set to false, 0, or no to hide.

data-versionstring

Track responses against specific application versions (e.g. 1.2.3, v2.0.0-beta). Stored on linked Rating / Feedback / SurveyResponse rows.

data-api-urlstringdefault:"auto-detect"

Override the API endpoint. Useful for self-hosted or staging environments.

Triggers (configured per survey)

Each survey has its own trigger configuration. The widget evaluates triggers on every survey returned by the config endpoint and shows the first one that matches (one survey at a time).

URL path patterns

Glob-style patterns matched against window.location.pathname:

  • * matches any sequence within a single path segment
  • ** matches across segments
/pricing
/billing/**
/account/*/settings

Custom event names

Names the widget listens for. Fire them from your own code at meaningful milestones:

javascript
window.SeggwatSurvey.trigger("checkout_complete");
window.SeggwatSurvey.trigger("first_export");

Time-on-page

Show the survey N seconds after the widget initializes if no earlier trigger fires.

Question Kinds

The widget renders any combination of these question shapes:

Kind Input Notes
open_text textarea Honors max_chars and placeholder
nps 0–10 button row Shows "Not likely" / "Very likely" labels
scale min–max button row Custom low_label / high_label shown beneath
single_choice radio list One option from a list
multi_choice checkbox list Multiple, with optional min / max cardinality
boolean two-button toggle Custom true_label / false_label

Choice options can set allow_other_text: true. When the user picks that option, a free-text "Other:" input appears beneath the choices and is submitted as other_text on the answer.

Each question can also be marked required. The Next button stays disabled until the answer is non-empty; non-required questions show a Skip button.

Thank-You Actions

After the last question, the widget shows the server-resolved thank-you action:

  • Message — headline + optional body text
  • DiscountOffer — headline, coupon code (monospace, dashed border), CTA button. The widget stays open longer (12s) before auto-hiding to give the user time to copy the coupon
  • Reactivate — headline + reactivate CTA (typically for win-back flows)
  • Custom — arbitrary HTML (only org admins can author surveys, so this is trusted to the same extent as the survey definition)

Examples

html
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"></script>
html
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"
        data-mode="modal"></script>

Inline inside a specific container

html
<div id="survey-slot"></div>

<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"
        data-mode="inline"
        data-container="#survey-slot"></script>

Force a specific survey, bypass its triggers

html
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"
        data-survey-id="65cf...e9"
        data-mode="inline"
        data-container="#exit-survey"></script>

Dev / QA (bypass cooldown across all surveys)

html
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
        data-project-key="your-project-key"
        data-cooldown-days="0"></script>

JavaScript API

The widget exposes window.SeggwatSurvey for programmatic control:

Identify the user

javascript
SeggwatSurvey.setUser("user_12345");

User IDs are opaque — max 255 chars, alphanumeric + hyphens + underscores. Never send PII.

Fire a custom event trigger

javascript
SeggwatSurvey.trigger("onboarding_done");

Dispatches a seggwat:survey:<name> event on window. Any survey whose event_names contains that name will be shown (subject to cooldown + sample rate).

Show a specific survey manually

javascript
SeggwatSurvey.show("survey_id");                  // respects cooldown
SeggwatSurvey.show("survey_id", { force: true }); // bypass cooldown (dev only)
SeggwatSurvey.hide();

Inspect state

javascript
SeggwatSurvey.hasAnswered("survey_id"); // boolean — inside cooldown window?
SeggwatSurvey.surveys;                  // read-only snapshot of live surveys

Reset local state

Clears this visitor's per-survey cooldown / sample-roll stored in localStorage. Does not delete server-side data.

javascript
SeggwatSurvey.reset();

How It Works

  1. Script loads, auto-detects its base URL, bootstraps seggwat-core.js if not already present.
  2. Fetches the live surveys list from GET /api/v1/projects/{project_key}/surveys-config.
  3. For each survey, evaluates triggers in order: URL pattern match (immediate), then arms time-on-page timer + listens for event names.
  4. On match: checks localStorage cooldown + sticky sample-rate roll. If both pass, displays the survey in the configured mode.
  5. User steps through one question per screen (Prev / Next), with required validation.
  6. On submit, POSTs to /api/v1/surveys/submit with project_key + survey_id + answers.
  7. Server atomically writes: one Rating per scale / NPS / boolean answer, one combined Feedback row for all open-text answers (tagged source: Survey), and one SurveyResponse sidecar linking everything.
  8. Widget renders the resolved thank_you action and auto-hides after a short grace.

API Endpoints

Submit a response

POST /api/v1/surveys/submit

Payload (client-triggered path):

json
{
  "project_key": "uuid",
  "survey_id": "65cf...e9",
  "answers": [
    { "question_id": "q1", "value": { "type": "nps", "value": 9 } },
    { "question_id": "q2", "value": { "type": "text", "value": "Loved the trial" } }
  ],
  "completed": true,
  "path": "/dashboard",
  "version": "1.2.3",
  "submitted_by": "user_12345",
  "locale": "en"
}

Response (201 Created):

json
{
  "response_id": "...",
  "feedback_id": "...",
  "rating_ids": ["...", "..."],
  "thank_you": {
    "type": "message",
    "headline": "Thanks!",
    "body": "Your response has been recorded."
  }
}

feedback_id is only present when at least one open-text answer was given. thank_you is null if the visitor didn't qualify (e.g. partial submission with show_to: completers).

Fetch the widget config

GET /api/v1/projects/{project_key}/surveys-config

Public, no auth. Returns the live, ClientSide-triggered surveys for the project plus their triggers, cooldown, sample rate, copy overrides, and thank-you actions. Returns an empty list when the project is not on Pro.

Track impressions and dismissals

POST /api/v1/surveys/events

Public, no auth. Batches shown / dismissed events from the widget so the dashboard can show response and dismissal rates next to raw submission counts. The widget fires these automatically — you don't need to call this endpoint yourself.

Payload:

json
{
  "project_key": "uuid",
  "events": [
    { "survey_id": "65cf...e9", "kind": "shown" },
    { "survey_id": "65cf...e9", "kind": "dismissed" }
  ]
}

Response (202 Accepted):

json
{ "accepted": 2 }

The third event kind, submitted, is incremented server-side from POST /surveys/submit. Don't send it from the widget.

Privacy and cookies

The Survey widget is built to keep customer pages free of consent-banner surface area. Concretely:

  • No cookies are set. Per-survey cooldown / sample-rate state is stored in localStorage, which under EU guidance is considered "strictly necessary" for delivering the feature the visitor is interacting with.
  • No tracking identifiers leave the browser. The widget never sends an IP, user agent, fingerprint, or anonymous visitor ID as part of an event. Server logs naturally capture IP for short-lived rate limiting, but that data is never persisted into the analytics counters.
  • Event counters are pure aggregates. Impressions and dismissals are stored only as (project, survey, day, event_kind, count) rows. There is no way to reconstruct who saw which survey from that data, which means the counters are not personal data under the GDPR.
  • submitted_by is opt-in. If you call SeggwatSurvey.setUser("...") or pass data-user-id, that identifier rides along on the submitted response. It does not appear on impression / dismissal events.

If your privacy policy already covers SeggWat for feedback / ratings, you typically do not need to add a separate disclosure for survey event counters — the same "non-identifying aggregate analytics" framing applies.

Data Model

Each submission writes up to three correlated row types:

  • Rating — one row per scale / NPS / boolean answer, feeding the existing aggregation pipelines (type: "scale" | "nps" | "helpful").
  • Feedback — a single combined row for all open-text answers in the submission, formatted as **prompt**\nanswer\n\n**prompt**\nanswer. Tagged source: "Survey". Flows through the regular triage queue.
  • SurveyResponse sidecar — links every Rating + Feedback row, denormalizes every answer (including its prompt_snapshot), and carries completed, locale, path, version, submitted_by.

Question identity is preserved via stable string ids on each SurveyQuestion. Reordering, renaming, or rewording questions in the dashboard later doesn't corrupt analytics — historical responses keep referencing the original question_id and carry their own prompt snapshot.

Best Practices

Use Cases

Activation Pulse

Fire SeggwatSurvey.trigger("activation_complete") after the user completes onboarding. A 3-question survey (clarity / value / blockers) captures impressions while they're fresh.

Pricing-Page Intent

URL trigger on /pricing. Ask what they're evaluating and the biggest blocker. Pair with a DiscountOffer thank-you on completion.

Feature Feedback

Custom-event trigger after the user uses a new feature N times. Tight, focused: clarity / usefulness / what's missing.

Pre-Cancel Save

URL trigger on /account/cancel. Open-text "what's not working" + a reactivate CTA in the thank-you. Catches churn intent before it lands.

Troubleshooting

Navigation