TL;DR
SeggWat surveys fire one of two ways — picked in the dashboard under Trigger → When does this survey fire?
| Frontend trigger (recommended) | Backend trigger (advanced) | |
|---|---|---|
| Where the trigger lives | In the browser, evaluated by the widget | On your server, posted to SeggWat's API |
| What you install | <script src="…/seggwat-survey.js"> once |
The script plus server-side API calls |
| What fires it | URL pattern, custom JS event name, or time-on-page | Predefined backend event (Trial expired, Subscription cancelled, Onboarding completed, …) or a custom event posted from your backend |
| Delivery | In-app widget while the user is on your site | In-app widget on next session and/or the magic-link URL returned in the dispatch response, for you to relay via your own channel (email, SMS, push, …) |
| Code you write | One <script> tag. Optional one-liner JS calls. |
Backend HTTP calls to users/upsert and (for right-now events) lifecycle/dispatch |
| Best for | NPS on /pricing, CSAT after onboarding, "was this helpful" everywhere, anything that fires while the user is in-product |
Reaching churned users, post-trial follow-ups, anything that should reach the user via your own notification stack regardless of whether they open your app |
This page covers the Backend trigger path. If you only need the frontend trigger, see the Survey widget docs — that path is one <script> tag and no backend work.
Looks-easier-in-the-UI is a trap. "Backend trigger" is one radio click in the dashboard but the actual events have to come from somewhere — that somewhere is your backend, calling SeggWat's API. The integration is real work; the dashboard just records which events should fire which survey.
SeggWat does not deliver the survey to your users on your behalf. The backend-trigger mode returns the magic-link URL in the API response so you can relay it via your own ESP (SendGrid, Postmark, SES, Resend, …) or whichever channel you already use for transactional messages. You stay in control of sender domain, deliverability, branding, unsubscribe, and the legal basis for the message. The widget can also surface those invitations in-app on the user's next visit — but only when the host page hands it the tokens (see Step 4). The widget does not auto-fetch them.
Trigger mode 1 — Frontend trigger
What you're signing up for
You install the Survey widget on your site once. The widget reads your live surveys from SeggWat on load and watches for whichever conditions you configured: URL match, custom event, or time-on-page. When a match fires, the survey renders in-product (banner, modal, or inline).
That's the whole integration. No backend code, no API keys in your server, no plumbing — the entire integration is the script tag.
Install once
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"></script>Identify the respondent if you have an internal user id — survey responses come back attributed:
SeggwatSurvey.setUser("user_abc123");Configure the trigger in the dashboard
In the dashboard go to Widgets → + New survey, pick a template, and on the Trigger tab choose Frontend trigger. You then fill in any combination of:
- URL patterns — globs like
/pricing/*,/checkout,/docs/**. Matches againstwindow.location.pathname. - Event names — string identifiers like
onboarding_donethat your code emits viaSeggwatSurvey.trigger("onboarding_done"). - Show after seconds on page — a dwell threshold before the survey is allowed to appear.
Flip the survey to Live and you're done in the dashboard.
Optionally fire JS events from your own code
URL pattern matching is fully automatic. JS event triggers are the escape hatch for moments that aren't a page navigation:
// Final step of onboarding flow
SeggwatSurvey.trigger("onboarding_done");
// User just clicked "Cancel subscription" — fire before the redirect
SeggwatSurvey.trigger("subscription_cancelled_in_app");Safe to call on every render — cooldown + sample-rate dedupe the actual display.
When this mode is the right call
- The user is in your app at the moment you want to ask the question.
- In-app rendering only is fine — no need to reach inboxes.
- You don't need follow-up to users who never log in again.
Trigger mode 2 — Backend trigger
What you're signing up for
You install the Survey widget the same way as mode 1 — plus you call SeggWat's HTTP API from your backend whenever a tracked event happens. SeggWat then:
- Returns a magic-link URL in the dispatch response. You hand it off to your own notification pipeline (email, SMS, Slack, in-product message, push, …) to reach users who aren't currently in your app.
- Holds the invitation server-side so the widget can render it in-app on the user's next visit — but only if your host page hands the widget the pending tokens (see Step 4). The widget does not auto-fetch them when you call
setUser().
This is the mode that can reach users who have stopped logging in — but only if your backend relays the link.
Step 1 — Install the widget (same as mode 1)
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"></script>The widget is still required for in-app rendering, but it does not auto-discover pending invitations from setUser() alone — you wire those up in Step 4.
Step 2 — Register the user with SeggWat (optional, but recommended)
POST /api/v1/users/upsert registers a user against the project. Two things this gets you:
- Trial-ending-soon scheduling — set
trial_ends_atand SeggWat will create aTrial ending sooninvitationNdays before the deadline (per-survey setting). - Email correlation in the dashboard — if you also pass
email, it shows next to in-app responses so triage can attach the address. SeggWat does not deliver anything to this address.
curl -X POST https://seggwat.com/api/v1/users/upsert \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "507f1f77bcf86cd799439011",
"user_ref": "user_abc123",
"email": "alex@acme.com",
"trial_ends_at": "2026-06-15T00:00:00Z"
}'The user_ref is your own stable user id — whatever you'd pass to SeggwatSurvey.setUser(...) on the client. Re-upsert on every meaningful state change; the endpoint is idempotent.
Step 3a — Fire "right-now" events from your backend
For events that happen now — Trial expired, Subscription cancelled, Onboarding completed, custom events — POST /api/v1/lifecycle/dispatch from your backend at the moment they occur:
curl -X POST https://seggwat.com/api/v1/lifecycle/dispatch \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "507f1f77bcf86cd799439011",
"user_ref": "user_abc123",
"event": "subscription_cancelled"
}'The response carries a created_invitations array — one entry per matching survey. Each entry has a magic_link_url ready to be passed to your own ESP:
{
"matched_surveys": 1,
"created_count": 1,
"suppressed_by_cooldown_count": 0,
"suppressed_by_sample_count": 0,
"errored_count": 0,
"created_invitations": [
{
"survey_id": "65b1f00000000000000000aa",
"user_ref": "user_abc123",
"magic_link_url": "https://seggwat.com/surveys/abc...43chars",
"expires_at": "2026-06-26T11:22:00Z"
}
]
}Relay it via the channel you already use for transactional messages — e.g. Postmark:
for invitation in dispatch_response["created_invitations"]:
postmark.send_email(
From="feedback@yourapp.com",
To=user.email,
Subject="Quick favour before you go — 2 questions",
HtmlBody=render_template("survey_email.html", url=invitation["magic_link_url"]),
)Supported event names match the checkboxes in the dashboard:
| Event in dashboard | API value |
|---|---|
| Trial expired | "trial_expired" |
| Trial cancelled | "trial_cancelled" |
| Subscription cancelled | "subscription_cancelled" |
| Subscription downgraded | "subscription_downgraded" |
| Onboarding completed | "onboarding_completed" |
| First feedback submitted | "first_feedback_submitted" |
| Custom event | { "custom": "your_name_here" } |
Step 3b — Schedule a "heads-up" before trial expiry
For Trial ending soon — fire N days before a known future date — you don't dispatch anything at all. You upsert the user with trial_ends_at and SeggWat creates the invitation for you. The magic-link URL will be available via GET /api/v1/projects/{project_id}/pending-invitations so your relay job can pick it up alongside the user's other pending notifications.
curl -X POST https://seggwat.com/api/v1/users/upsert \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
-d '{
"project_id": "...",
"user_ref": "user_abc123",
"email": "alex@acme.com",
"trial_ends_at": "2026-06-15T00:00:00Z"
}'The configured warn-days on the survey (default: 3) controls when the invitation actually drops. If the user extends, re-upsert with the new trial_ends_at — the old schedule is invalidated automatically. If they convert or cancel, upsert with trial_ends_at: null.
Step 4 — (optional) Surface pending invitations in-app
If you also want the widget to render pending invitations the next time the user is in your app (recommended — it's free reach for users who never open the email), your backend has to feed the widget the tokens. The widget does not poll on its own.
Two server calls and one snippet of host-page glue:
1. Backend: fetch pending tokens for the user. Authenticated with your API key:
curl -G https://seggwat.com/api/v1/projects/$PROJECT_ID/pending-invitations \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
--data-urlencode "user_ref=user_abc123"Returns:
{
"invitations": [
{ "token": "abc...43chars", "survey_id": "65...", "triggered_by": "trial_expired", "expires_at": "2026-06-26T11:22:00Z", "in_app_shown_at": null }
]
}2. Host page: hand the tokens to the widget. Server-render them into the script tag, or set the global, or call the JS API after auth — whichever fits your stack:
<!-- Option A: render into the script tag -->
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"
data-pending-tokens="abc...43chars,def...43chars"></script><!-- Option B: set the global before/after the script loads -->
<script>window.__seggwat_pending_invitation_tokens = ["abc...43chars"];</script>
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"></script>// Option C: call the JS API after authenticating the user
SeggwatSurvey.setUser("user_abc123");
SeggwatSurvey.setPendingInvitations(["abc...43chars"]);The widget resolves each token via /api/v1/surveys/by-token/{token} and renders the first one. If the user dismisses or submits, the server stamps in_app_shown_at so the email follow-up is suppressed for that invitation.
Why this lives in your backend and not the widget: pending-invitations is API-key auth — only your server can safely map your auth context → user_ref. Letting an unauthenticated browser look up invitations by user_ref alone would let anyone who guesses a user id enumerate pending surveys.
When this mode is the right call
- The user might not be in your app at the moment that matters (post-churn, post-trial-expiry).
- You want to relay the survey via your own email/SMS/push stack.
- You're comfortable adding two HTTP calls to your backend on signup / billing-state-change.
- You want a fixed backend-event vocabulary (trial expired, subscription cancelled, …) instead of free-form JS event names.
Why this is "advanced"
The UI gives you a list of checkboxes that look done-deal: tick "Trial expired", save, ship. But the dashboard cannot fire those events on its own — your backend has to call lifecycle/dispatch (or users/upsert with trial_ends_at for the scheduled case) the moment they actually happen. Otherwise the survey sits Live and never fires for anyone.
If you don't want to write that backend code, use the Frontend trigger instead and call SeggwatSurvey.trigger("trial_ended") from your client when the user lands on the post-trial paywall page. Same end result, no backend integration.
Side-by-side example: the same "trial ended" survey
Same survey content, two trigger setups — pick the one that matches your reality.
As a frontend trigger
Dashboard: Trigger = Frontend trigger. URL patterns: /billing/expired. Event names: (none).
Your app code:
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"></script>When the user lands on /billing/expired, the widget matches the URL pattern and fires the survey. Zero backend integration.
As a backend trigger
Dashboard: Trigger = Backend trigger. Events: Trial expired.
Your backend (any language):
// On signup — register the user (optional but useful for trial-ending-soon scheduling and email correlation)
await fetch("https://seggwat.com/api/v1/users/upsert", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SEGGWAT_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
project_id: "507f1f77bcf86cd799439011",
user_ref: user.id,
email: user.email,
trial_ends_at: user.trialEndsAt,
}),
});
// Later, when your billing job flips the user to trial-expired
const res = await fetch("https://seggwat.com/api/v1/lifecycle/dispatch", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SEGGWAT_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
project_id: "507f1f77bcf86cd799439011",
user_ref: user.id,
event: "trial_expired",
}),
}).then((r) => r.json());
// Relay the magic link via your own ESP
for (const invitation of res.created_invitations) {
await yourMailer.send({
to: user.email,
subject: "30% off if you've got 2 minutes",
html: `<a href="${invitation.magic_link_url}">Tell us why you didn't convert</a>`,
});
}The user receives the magic-link email immediately via your ESP. If you also wire up Step 4, they'll see the survey in-app on their next visit too — and the email follow-up gets suppressed once they engage with the in-app render. Three HTTP calls on your side (two SeggWat, one your mailer); works for users who never come back.
Combining both modes
The two modes aren't exclusive — you can have a /billing/expired URL-pattern survey and a backend-event survey for the same lifecycle moment. The widget only displays one survey per visit and respects cooldown across both, so doubling up is safe.
A common belt-and-braces pattern:
- Frontend trigger for users who do come back to the app — instant feedback, no notification lag.
- Backend trigger (relayed via your own ESP) for users who don't come back — picks up the long tail.
Both surveys can be linked to the same underlying question set if you author them as duplicates.
Troubleshooting
"My backend-triggered survey never fires." — Check, in order:
- The survey is Live in the dashboard.
- Your backend is actually calling
POST /api/v1/lifecycle/dispatchat the right moment. There is no automatic dispatch from billing webhooks today — your code has to do this call. - The
eventvalue in your dispatch payload matches the survey's configured event (exact string match, case-sensitive). - The dispatch response had
created_count > 0. A non-zerosuppressed_by_cooldown_countorsuppressed_by_sample_countmeans SeggWat intentionally skipped — re-check those settings.
"The dispatch returned a magic-link URL but the user never got the email." — That's your mailer, not SeggWat. SeggWat stops at handing you the URL; everything after that (sender domain, deliverability, list management) is your ESP's job.
"My frontend-triggered survey never fires." — Check, in order:
- The survey is Live with trigger source Frontend trigger.
- The URL pattern matches
window.location.pathname(try in the browser console). - Or, your code is calling
SeggwatSurvey.trigger("event_name")with the exact event name configured in the survey. window.SeggwatSurveyis defined — the script tag is on the page.- The user isn't inside the cooldown window. Call
SeggwatSurvey.reset()in the console to clear local cooldown state. - The sample-rate roll passed.
reset()re-rolls.
"SeggwatSurvey.show(surveyId, { force: true })" — addresses a survey directly by id, bypassing trigger matching, cooldown, and sample-rate. Use it for QA, conditional surfaces, or to render an arbitrary survey from custom UI.
Reference
- Survey widget JS API —
trigger(),show(),setUser(),setPendingInvitations(),reset() POST /api/v1/users/upsert— register a user and their trial deadlinePOST /api/v1/lifecycle/dispatch— fire a backend event right now; returns magic-link URLsGET /api/v1/projects/{project_id}/pending-invitations?user_ref=...— list pending invitation tokens for a user, to render in-app via the widget
