Web SDK Installation
Install the SaaSFunnels Web SDK for supplemental browser events.
The Web SDK is a supplemental event source for browser-observable product moments such as activation, pricing intent, checkout starts, downgrade or cancel intent, team invites, and limit friction. Keep backend systems, Stripe, PostHog, Segment, or Direct API events as the primary source when they already provide cleaner account-level data.
Create a client
In SaaSFunnels, open Settings → Integrations → Web SDK, create a client, and add the exact origins where the customer app runs. The client key is publishable and origin-limited, but it is shown only once so the install snippet can be copied safely.
If you are following the Setup checklist, Connect Your App explains where the browser key and Client ID go, how the separate Funnel server key is stored, and how Setup verifies the connection. For a Next.js app, the browser key can use NEXT_PUBLIC_SAASFUNNELS_WEB_SDK_CLIENT_KEY; other frameworks use their own public configuration mechanism.
Never place Direct API, PostHog, Segment, Stripe, or server ingest secrets in browser code.
Use SaaSFunnels MCP Server when a coding agent needs source-boundary context. The SaaSFunnels CLI validates Direct API, PostHog, and Segment payloads; the Web SDK remains the browser-safe path for visible intent and must not receive server ingest keys.
Install the script
Add the hosted script to the customer application and initialize it with the Web SDK client key.
<script>
window.saasfunnels = window.saasfunnels || [];
window.saasfunnels.push(["group", "acct_123"]);
window.saasfunnels.push([
"init",
{
clientKey: "<web_sdk_client_key>",
host: "https://app.saasfunnels.ai",
autoPage: false,
},
]);
window.saasfunnels.push([
"track",
"upgrade_clicked",
{
plan: "growth",
source: "pricing",
},
]);
</script>
<script async src="https://app.saasfunnels.ai/sdk/saasfunnels.js"></script>If the website uses Content Security Policy, allow this SaaSFunnels host for the stable script and its immutable module release. Preserve the host nonce when using nonce-based CSP. Set Funnel consent to granted only after the site's consent owner permits this purpose; traffic entries do not evaluate while consent is pending or denied.
Install website Funnel entries
Create the website behavior in Funnel Studio → Targeting, then register its exact placement, named event, and typed detector policy in the customer website.
saasfunnels.funnels.setConsent("granted");
const unregister = saasfunnels.funnels.registerPlacement({
key: "pricing_recovery",
trafficEntries: [
{
eventKey: "website.desktop_exit_intent",
kind: "desktop_exit_intent",
},
{
eventKey: "website.time_on_page",
kind: "time_on_page",
seconds: 15,
},
{
eventKey: "website.scroll_depth",
kind: "scroll_depth",
percent: 60,
},
{
eventKey: "website.inactivity",
kind: "inactivity",
seconds: 30,
},
{ eventKey: "website.route", kind: "route" },
],
});For an existing named website interaction that should be eligible by page or campaign context, opt in on its placement:
saasfunnels.funnels.registerPlacement({
key: "pricing_recovery",
triggers: ["pricing_cta_clicked"],
websiteContext: true,
});The opt-in is explicit so existing host-interaction payloads remain unchanged by default.
Desktop exit intent runs only for a fine pointer leaving through the top of the viewport and is bounded to once per first-party session. It never runs for touch or coarse-pointer devices. Choose separately named Scroll depth or Inactivity entries for mobile; SaaSFunnels does not relabel those as exit intent.
The runtime sends only normalized path or route key, referrer origin and path, five allowlisted UTM fields, coarse device class, lawful new/returning session class, a first-party session identifier, exact placement, and the trigger name/value. Raw URLs, raw query strings, fragments, credentials, form values, and arbitrary DOM are dropped. Route changes work for initial loads and SPAs, and unregister or runtime teardown removes every listener and timer.
Return to Settings → Integrations → Web SDK to see the exact placements and triggers detected by runtime decisions. Funnel Validation and Test distinguish a missing installation, missing placement, unseen trigger, stale binding, and verified current draft.
Install an acquisition Funnel
The governed acquisition starters use the same runtime and Studio as in-product Funnels:
| Starter | Default placement and entry |
|---|---|
| Lead magnet | guides.activation_benchmark.inline; scroll depth at 60% |
| Demo request | pricing.demo_request.inline; pricing route |
| Waitlist | product.early_access.inline; early-access route |
| Pricing-page recovery | pricing.exit.modal; desktop exit intent |
For Inline capture, reserve an exact host-owned mount and pass the element itself. Your page owns its width, spacing, and reserved height; the Studio sample container is illustrative and does not set your site's layout. Use the placement key shown in Studio's Set up embed view.
Registration reserves a delivery location. Request a decision explicitly with check, an installed trigger, or React's opt-in checkOnMount; mounting alone does not request one. Keep the existing SDK client/provider and consent setup above.
<div id="activation-guide-funnel" style="min-height: 12rem"></div>
<script>
const mount = document.getElementById("activation-guide-funnel");
const unregister = saasfunnels.funnels.registerPlacement({
autoCheckOnRoute: true,
key: "guides.activation_benchmark.inline",
mount,
trafficEntries: [
{
eventKey: "website.scroll_depth",
kind: "scroll_depth",
percent: 60,
},
],
});
void saasfunnels.funnels.check("guides.activation_benchmark.inline");
</script>React applications use the same runtime and a visible Inline mount:
import {
FunnelInlinePlacement,
SaaSFunnelsProvider,
} from "@saasfunnels/funnels-react";
<SaaSFunnelsProvider
config={{
clientKey: "<web_sdk_client_key>",
consent: consentGranted ? "granted" : "pending",
host: "https://app.saasfunnels.ai",
}}
>
<FunnelInlinePlacement
autoCheckOnRoute
checkOnMount
className="min-h-48"
placementKey="product.early_access.inline"
/>
</SaaSFunnelsProvider>;Before publishing:
- Add every production and preview origin to the Web SDK client and allow the SaaSFunnels script, module, API, and optional renderer stylesheet under the site's CSP.
unsafe-evalis not required. - Keep consent
pendinguntil the site's consent owner grants the configured capture and delivery purpose. Withdrawal closes active UI and prevents new decisions. - Match the Studio placement key exactly and configure page/campaign rules as eligibility. Do not put raw URLs, query strings, form answers, or arbitrary DOM values into website context.
- Connect an approved
lead_handofforlead_magnet_deliverydestination. The first release references an external asset or provider template; SaaSFunnels does not host uploaded lead-magnet files. - Use Inline for forms. Banner supports a compact announcement or native CTA and deliberately does not accept capture fields.
- Keep desktop exit intent desktop-only. Configure a separately named route, scroll-depth, time-on-page, or inactivity entry for mobile traffic.
After publishing, verify the current draft in Studio Validation, submit one consented test Lead, and reconcile Leads → Responses, Delivery, and Outcomes with Funnel Results and provider Integration Activity. If the Funnel does not render, inspect Web SDK diagnostics for the exact origin, consent state, placement, route, trigger, current definition version, runtime version, CSP block, cap, cooldown, conflict, or operational-control reason. Operators should follow the Acquisition Funnel release runbook for rollback and recovery.
Identify accounts
Every useful revenue event needs a stable account identity. Call saasfunnels.group("acct_123", traits) once the account, workspace, company, or tenant is known. You can also pass account_id on individual track calls.
Accountless events are stored as setup diagnostics instead of becoming revenue signals.
Send account traits
Use saasfunnels.group(accountId, traits) to send the ordinary SaaS account facts SaaSFunnels needs for account tracking and signal quality. Refresh these traits when the account signs up, changes plan, enters or exits trial, changes team size, or loads the authenticated app shell.
window.saasfunnels.group("acct_123", {
account_created_at: "2026-06-01T12:00:00.000Z",
plan: "starter",
is_in_trial: true,
trial_ends_at: "2026-06-28T12:00:00.000Z",
user_count: 7,
active_user_count: 6,
seat_count: 10,
});Minimum useful traits:
| Fact | Preferred field names | Notes |
|---|---|---|
| Customer signup date | account_created_at, signup_at, or customer_created_at | This is the customer product signup date, not the time SaaSFunnels first saw the account. |
| Login activity | Track user_logged_in or session_started | Include a stable user ID with identify or track options when available. |
| Plan and lifecycle | plan, plan_name, current_plan, account_status, or subscription_status | Keep values bounded and stable. |
| Trial state | is_in_trial, trial_started_at, trial_ends_at | Use as a fallback when Stripe is not connected. Stripe is authoritative for billing and trial state when connected. |
| Team and seats | user_count, active_user_count, seat_count, or member_count | Send counts only, not raw user lists. |
Track events
Start with explicit events that map to revenue semantics:
window.saasfunnels.track("user_logged_in", {
source: "app_shell",
});
window.saasfunnels.track("checkout_started", {
plan: "growth",
value: 1,
});Generic page views are not treated as revenue signals by default. Use explicit names such as pricing_viewed, upgrade_clicked, cancel_flow_started, or team_member_invited when the page or action reflects intent.
Sparse Event KPI events
SaaSFunnels does not need full clickstream telemetry to produce Event KPI analytics. Start with a small allowlist of account-scoped events that describe value, friction, expansion intent, and sentiment.
| Event family | Example names | Useful properties |
|---|---|---|
| High-value usage | feature_used, api_credits_used, report_exported, workflow_completed, generation_completed, sync_completed | feature_key, feature_area, is_key_feature, is_premium, value, quantity, usage_ratio, allowance, sentiment fields |
| Activation / first value | account_activated, first_value_completed, onboarding_completed | plan, feature, value, sentiment fields |
| Limit friction | limit_hit, quota_blocked, overage_prompt_viewed | usage_ratio, allowance, plan, value, sentiment fields |
| Product or support friction | sync_failed, setup_failed, support_needed | feature, error_code, severity, sentiment fields |
| Upgrade intent | pricing_viewed, upgrade_clicked, premium_feature_clicked, premium_feature_used, checkout_started | plan, current_plan, target_plan, feature_key, value, sentiment fields |
| Downgrade or cancel intent | downgrade_flow_started, cancel_flow_started | plan, reason_code, sentiment fields |
| Team expansion | team_member_invited, seat_added, role_assigned | quantity, role, plan, sentiment fields |
| Integration connected | integration_connected, oauth_connected, integration_configured | integration, provider, sentiment fields |
| Checkout outcome | checkout_started, checkout_completed, checkout_abandoned | plan, value, currency, sentiment fields |
These same event names and properties work for the Web SDK, Direct API, PostHog, and Segment. The source-specific wrapper can change, but the account identity, event name, timestamp, value fields, semantic intent, and sentiment fields should stay consistent.
Flag key features
Use key-feature fields on high-value usage, premium intent, or limit-friction events when the feature itself explains account health, expansion readiness, or churn risk. Do not create dynamic event names such as generation_feature_used or one event name per feature. Keep the event name stable, then describe the feature with bounded properties.
For Web SDK events, call saasfunnels.group(accountId, traits) before tracking, then send:
feature_key: stable machine name for the feature, such asgeneration,export,workflow, orapi.feature_area: stable product area, such asai,reporting,automation, orintegrations.is_key_feature:trueonly when the feature is part of the customer's core value path.is_premium:truewhen the feature is paid, gated, or upgrade-relevant.value,quantity,usage_ratio, orallowancewhen the action has measurable volume or usage pressure.
window.saasfunnels.group("acct_123", { plan: "starter" });
window.saasfunnels.track("feature_used", {
feature_key: "generation",
feature_area: "ai",
is_key_feature: true,
is_premium: true,
value: 1,
});Use the Web SDK for browser-visible feature clicks, premium-feature intent, or UI friction the backend cannot see. Use Direct API instead when the backend can confirm completed usage, billable metering, export completion, workflow completion, quota calculation, or a durable setup milestone.
Sentiment fields
Sentiment is optional and separate from the event family. Missing sentiment is stored as unknown, not neutral; use neutral only when the customer action explicitly carries neutral sentiment.
Send sentiment only when the browser action has a bounded, defensible signal. If sentiment is omitted, SaaSFunnels may infer a mapping default from the event name, semantic type, event kind, and bounded property keys. That mapped default is reviewable in the Events page and fills future events only when payload sentiment is missing or unknown.
window.saasfunnels.track("api_credits_used", {
value: 250,
usage_ratio: 0.82,
allowance: 300,
feature_key: "generation",
feature_area: "ai",
is_key_feature: true,
sentiment: "negative",
sentiment_score: -0.6,
sentiment_confidence: 0.8,
sentiment_source: "explicit",
});Accepted sentiment fields:
| Field | Values |
|---|---|
sentiment | positive, neutral, negative, unknown |
sentiment_score | Number from -1 to 1 |
sentiment_confidence | Number from 0 to 1 |
sentiment_source | explicit, inferred, ai, integration, unknown |
Do not send free-text feedback, support transcripts, chat messages, form fields, or raw customer comments as sentiment context. Send a bounded label, score, confidence, and source instead.
KPI coverage
Each Event KPI improves with a few specific event families. More volume helps, but variety and account identity matter more than tracking every click.
| KPI | Minimum useful coverage |
|---|---|
| Sentiment Trends | Any revenue-relevant event family with sentiment fields across 7, 14, or 30 day windows |
| Sentiment-Weighted Usage | High-value usage events with value, quantity, or usage_ratio, plus optional sentiment |
| Revenue-Sentiment Divergence | Usage, checkout, upgrade, downgrade, or cancel intent events combined with billing or plan context |
| Friction Load | Limit, support, setup, sync, checkout, or payment-friction events plus value events for comparison |
| Expansion Surface Area | Upgrade intent, top-ups, team expansion, integration connected, usage pressure, and positive sentiment |
| Account Momentum Quality | A mix of usage, activation, expansion, friction, and sentiment events over trailing windows |
As a rule of thumb, one or two isolated events are diagnostic only. Three or more account-scoped events in a 14 day window can become directional. Five or more relevant events in 30 days, especially across two or more event families, can support usable KPI confidence. Low-volume accounts should still show sample size and low-evidence states instead of being forced into a strong classification.
See Event KPI Signals for the canonical KPI definitions and account filter fields.
Source-compatible examples
Direct API payloads can send the normalized field names directly:
{
"account_id": "acct_123",
"event_id": "evt_usage_001",
"event_name": "api_credits_used",
"timestamp": "2026-06-20T15:00:00.000Z",
"value": 250,
"usage_ratio": 0.82,
"sentiment": "negative",
"sentiment_score": -0.6,
"sentiment_confidence": 0.8,
"sentiment_source": "explicit"
}PostHog and Segment should forward the same business fields inside properties while keeping a stable account or group identity:
{
"event": "api_credits_used",
"properties": {
"account_id": "acct_123",
"account_created_at": "2026-06-01T12:00:00.000Z",
"active_user_count": 6,
"is_in_trial": true,
"plan": "starter",
"seat_count": 10,
"trial_ends_at": "2026-06-28T12:00:00.000Z",
"value": 250,
"usage_ratio": 0.82,
"sentiment": "negative",
"sentiment_score": -0.6,
"sentiment_confidence": 0.8,
"sentiment_source": "integration"
},
"timestamp": "2026-06-20T15:00:00.000Z"
}{
"type": "track",
"event": "team_member_invited",
"groupId": "acct_123",
"userId": "user_456",
"properties": {
"quantity": 2,
"sentiment": "positive"
},
"timestamp": "2026-06-20T15:00:00.000Z"
}Data safety
The SDK sends event names, timestamps, normalized URL path/referrer context, account/user identifiers, and the properties you provide. It strips query strings and fragments from URL fields. Do not send form values, emails, session tokens, API keys, cookies, authorization headers, arbitrary route changes, every button click, or free-text support content.
Verify setup
After installation, send one event from an allowed origin and check Settings > Integrations > Web SDK and Events. A valid account-scoped event should appear as source web_sdk. If the event is missing account identity, SaaSFunnels will show a rejected diagnostic row with setup guidance.