saasco-sdk 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,974 @@
1
+ [Saasco](https://www.saasco.com) is an all-in-one marketing stack for SaaS companies. Connect once and get analytics, CRM, email, support, and ads — and only pay the infrastructure cost of what you use.
2
+
3
+ This package (`saasco-sdk`) is the browser/server SDK. One install covers event tracking, identify, optional ad pixels, the social-proof widget, and the AI support widget.
4
+
5
+ ---
6
+
7
+ - [Privacy](#privacy)
8
+ - [Getting Started](#getting-started)
9
+ - [Public methods](#public-methods)
10
+ - [Configuration](#configuration)
11
+ - [Automatic Page View Tracking](#automatic-page-view-tracking)
12
+ - [Manual Page Tracking](#manual-page-tracking)
13
+ - [Tracking Events](#tracking-events)
14
+ - [Identifying Users](#identifying-users)
15
+ - [Debugging and Dev](#debugging-and-dev)
16
+ - [Ad Integrations](#ad-integrations)
17
+ - [Social Proof Widget](#social-proof-widget)
18
+ - [Use the hosted SDK](#use-the-hosted-sdk)
19
+ - [Support Widget](#support-widget)
20
+ - [Reserved Properties](#reserved-properties)
21
+
22
+ <br />
23
+
24
+ ## Privacy
25
+
26
+ By default Saasco is privacy friendly, obscuring all activity behind anonymous IDs. Visitor and session IDs are stored in first-party cookies (`saasco-sdk-*`) on your site's root domain — about one year for the visitor id, 30 minutes for the session. If you want to power more complex user tracking you can identify users and then all events they do will be attributed to them in your CRM. This can assist with customer support but also for things like drip campaigns and other marketing activities.
27
+
28
+ ## Getting Started
29
+
30
+ Install by running:
31
+
32
+ ```bash
33
+ npm install saasco-sdk
34
+ ```
35
+
36
+ Copy your Project ID from the project settings page in Saasco:
37
+ [https://www.saasco.com/projects/your-project/apps/dashboard/settings/project](https://www.saasco.com/projects/your-project/apps/dashboard/settings/project)
38
+
39
+ Then import Saasco, initialize the lib with your Project ID, and call `init()` once in the browser. `init()` starts automatic page tracking and (when the dashboard toggles are on) the support and social-proof widgets. Constructing `new Saasco(...)` alone does not start page tracking or widgets. Configured ad pixels initialize in the constructor when `enabled` is `true`.
40
+
41
+ ```ts
42
+ // lib/saasco.ts
43
+ import { Saasco } from "saasco-sdk";
44
+
45
+ export const saasco = new Saasco({ projectId: "YOUR-PROJECT-ID" });
46
+ ```
47
+
48
+ ```ts
49
+ // Call once on the client — e.g. in your app root
50
+ import { saasco } from "./lib/saasco";
51
+
52
+ saasco.init();
53
+ ```
54
+
55
+ In Next.js App Router, wrap `init()` in a small client component so it only runs in the browser:
56
+
57
+ ```tsx
58
+ "use client";
59
+
60
+ import { useEffect } from "react";
61
+ import { saasco } from "../lib/saasco";
62
+
63
+ export function SaascoAnalytics() {
64
+ useEffect(() => {
65
+ saasco.init();
66
+ }, []);
67
+
68
+ return null;
69
+ }
70
+ ```
71
+
72
+ Then render `<SaascoAnalytics />` from your root layout.
73
+
74
+ ## Public methods
75
+
76
+ | Method | What it does |
77
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
78
+ | `init()` | Starts automatic page tracking and (when the dashboard toggles are on) injects the support and social-proof loaders. Constructing `new Saasco(...)` is not enough. |
79
+ | `track(...)` | Send a custom event. Client: `track(name, properties?)`. Server: object form with `userId` or `anonymousId`. See [Tracking Events](#tracking-events). |
80
+ | `identify(...)` | Tie events to a CRM contact. The same call is mirrored into the support widget. See [Identifying Users](#identifying-users). |
81
+ | `page()` | Manual page view. The event name is `Page View`. Browser-only; dedupes the last recorded path. See [Manual Page Tracking](#manual-page-tracking). |
82
+ | `logout()` | Sign-out helper: clears the user cookie, mints a new session and anonymous id, clears the support identity, and drops last-touch UTM / referrer. |
83
+ | `enableDebug()` | Turn on console logging after init. |
84
+ | `disableDebug()` | Turn off console logging. `?saasco-debug=true` still wins. |
85
+ | `getIntegrationsStats()` | Debug helper for ad-pixel queue and status. |
86
+
87
+ The support widget's host API (`window.SaascoSupport`) is documented under [Support Widget](#support-widget).
88
+
89
+ ## Configuration
90
+
91
+ ```ts
92
+ const saasco = new Saasco({
93
+ projectId: "YOUR-PROJECT-ID",
94
+ enabled: true,
95
+ debug: false,
96
+ debugVerbose: false,
97
+ proxy: "https://t.saasco.com/",
98
+ autoPageTracking: {
99
+ enabled: true,
100
+ trackQueryParams: true,
101
+ trackHash: true,
102
+ },
103
+ integrations: [],
104
+ });
105
+ ```
106
+
107
+ | Option | Default | Description |
108
+ | ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `projectId` | required | Project ID from Saasco settings. |
110
+ | `enabled` | `true` | When `false`, no events are sent and ad pixels are not initialized. Debug logging still works. |
111
+ | `debug` | `false` | Log analytics events to the console. Works even when `enabled` is `false`. |
112
+ | `debugVerbose` | `false` | More detailed logger output. |
113
+ | `proxy` | `https://t.saasco.com/` | Override the ingest host if you forward events through your own proxy. |
114
+ | `autoPageTracking` | `{ enabled: true, trackQueryParams: true, trackHash: true }` | Automatic page-view tracking. |
115
+ | `integrations` | `[]` | Facebook Pixel, TikTok Pixel, and/or Pinterest Tag. See [Ad Integrations](#ad-integrations). |
116
+ | `support` | — | Optional `baseUrl` / `scriptUrl` / `placeholder` for same-origin, proxied, or self-hosted installs. The dashboard toggle decides whether the widget loads. See [Support Widget](#support-widget). |
117
+ | `socialProof` | — | Optional `baseUrl` / `scriptUrl` for same-origin, proxied, or self-hosted installs. The dashboard toggle decides whether the widget loads. See [Social Proof Widget](#social-proof-widget). |
118
+
119
+ ## Automatic Page View Tracking
120
+
121
+ When you call `init()`, Saasco starts tracking page views. By default it tracks all URL changes, including query parameters and hash changes. You can customize this with `autoPageTracking`:
122
+
123
+ ```ts
124
+ const saasco = new Saasco({
125
+ projectId: "your-project-id",
126
+ autoPageTracking: {
127
+ enabled: true, // Set to false to disable automatic page tracking
128
+ trackQueryParams: true, // Set to false to ignore URL query parameter changes
129
+ trackHash: true, // Set to false to ignore URL hash changes
130
+ },
131
+ });
132
+
133
+ saasco.init();
134
+ ```
135
+
136
+ For most apps you can leave the defaults. Disable automatic tracking only if you need to [record page views yourself](#manual-page-tracking).
137
+
138
+ ## Manual Page Tracking
139
+
140
+ If you disable automatic page tracking (`autoPageTracking.enabled: false`), call `page()` yourself when the route changes. The event name is `Page View`. Do not leave auto tracking on and also call `page()` — you will double-count.
141
+
142
+ ```ts
143
+ const saasco = new Saasco({
144
+ projectId: "YOUR-PROJECT-ID",
145
+ autoPageTracking: { enabled: false },
146
+ });
147
+
148
+ saasco.init();
149
+
150
+ // After a client-side navigation
151
+ saasco.page();
152
+ ```
153
+
154
+ `page()` is browser-only. It dedupes against the last recorded path (including query/hash when those options are on), so calling it twice for the same URL is a no-op.
155
+
156
+ ## Tracking Events
157
+
158
+ With events you can track custom actions users are taking on your site with event properties. The track method supports both client-side (parameter-style) and server-side (object-style) usage patterns.
159
+
160
+ Each event has 2 components:
161
+
162
+ **Name** - this is the name of the action that was taken.
163
+
164
+ _We like to follow Segment's [Object Action Framework](https://segment.com/academy/collecting-data/naming-conventions-for-clean-data/) for naming events._
165
+
166
+ **Properties** - This is an object with the values of the event. Such as the value, currency, or query.
167
+
168
+ ### Client Usage
169
+
170
+ For client-side tracking, use the Segment-compatible syntax.
171
+ This will automatically assign userId based on identify calls during the session.
172
+
173
+ ```ts
174
+ import { saasco } from "../lib/saasco.ts";
175
+
176
+ saasco.track("Searched Movies");
177
+
178
+ saasco.track("Searched Movies", {
179
+ query: "batman",
180
+ sortBy: "relevancy",
181
+ });
182
+ ```
183
+
184
+ ### Server Usage (Object Style)
185
+
186
+ For server-side tracking, use the object-style syntax.
187
+ Pass a `userId` or an `anonymousId` (from the client) so there is a contact to connect the events to.
188
+
189
+ ```ts
190
+ import { saasco } from "../lib/saasco.ts";
191
+
192
+ saasco.track({
193
+ event: "Signed Up",
194
+ userId: "user_123",
195
+ properties: {
196
+ provider: "email",
197
+ },
198
+ });
199
+ ```
200
+
201
+ ## Identifying Users
202
+
203
+ By default Saasco does not track any identifiable data about users, just anonymous user ids and session ids. Identify a user to tie events to a CRM contact and power tools like drip campaigns and customer support.
204
+
205
+ Identify with their Distinct ID (usually the user ID from your database) and any properties on the user. The same call is mirrored into the support widget. The dashboard toggle only decides whether the widget loader downloads — it does not gate this forward. If you load the standalone support loader without the analytics SDK, call `window.SaascoSupport.identify(...)` yourself.
206
+
207
+ ```ts
208
+ import { saasco } from "../lib/saasco.ts";
209
+
210
+ function signedIn() {
211
+ saasco.identify("USER-ID-FROM-YOUR-DB", {
212
+ name: "Tony Hawk",
213
+ email: "tony@xgames.com",
214
+ bestTrick: 900,
215
+ });
216
+ }
217
+
218
+ function signedOut() {
219
+ saasco.logout();
220
+ }
221
+ ```
222
+
223
+ Call identify when a user logs in or their properties change — not on every page load. Duplicate identify calls with the same id and traits are skipped.
224
+
225
+ `logout()` is the sign-out helper: it calls `identify(null)` (which clears the user cookie, mints a new session and anonymous id, and clears the support identity) and also drops last-touch UTM / referrer (`super-context`). `identify(null)` does not POST an identify event.
226
+
227
+ ### Identifying users who don't have an ID
228
+
229
+ Often you have a user's contact details before they sign up — part of the signup flow, or a marketing-email subscribe.
230
+
231
+ Skip the ID and pass properties only — include an `email`. That is what makes the CRM contact useful for marketing and later merge.
232
+
233
+ ```ts
234
+ saasco.identify({
235
+ name: "Tony Hawk",
236
+ email: "tony@xgames.com",
237
+ bestTrick: 900,
238
+ });
239
+ ```
240
+
241
+ Once they log in with the same email, their properties are connected.
242
+
243
+ ### Soft vs Full identify
244
+
245
+ When users are identified without an `ID` we call this a "Soft Identify" — the SDK mints a `soft_*` distinct id. Pass an email so the CRM has a real contact. Once they sign up and you identify them with your database `ID`, they are fully identified. Later identifying the same email with a real ID merges the soft contact into that user.
246
+
247
+ Either call creates a CRM contact. The CRM does not label contacts as "Lead" vs "Customer" from this.
248
+
249
+ ## Debugging and Dev
250
+
251
+ To exclude local and staging traffic, turn analytics off with `enabled`:
252
+
253
+ ```ts
254
+ export const saasco = new Saasco({
255
+ projectId: "YOUR-PROJECT-ID",
256
+ enabled: process.env.NODE_ENV === "production",
257
+ });
258
+ ```
259
+
260
+ ### Debugging
261
+
262
+ Turn on debugging with `debug: true` (or `debugVerbose: true`) on the constructor, or append `?saasco-debug=true` / `?saasco-debug-verbose=true` to the page URL. URL params override the constructor.
263
+
264
+ ```ts
265
+ export const saasco = new Saasco({
266
+ projectId: "YOUR-PROJECT-ID",
267
+ enabled: process.env.NODE_ENV === "production",
268
+ debug: true,
269
+ });
270
+ ```
271
+
272
+ Debug logs still print when `enabled` is `false` — events just aren't sent.
273
+
274
+ You can also toggle after init:
275
+
276
+ ```ts
277
+ saasco.enableDebug();
278
+ // logs "Debug mode activated."
279
+
280
+ saasco.disableDebug();
281
+ // logs "Debug mode deactivated."
282
+ ```
283
+
284
+ `?saasco-debug=true` still wins over `disableDebug()`.
285
+
286
+ ## Ad Integrations
287
+
288
+ The SDK can load Facebook Pixel, TikTok Pixel, and Pinterest Tag and forward `track` / `identify` calls to them. Map your Saasco event names to each network's standard events.
289
+
290
+ ```ts
291
+ export const saasco = new Saasco({
292
+ projectId: "YOUR-PROJECT-ID",
293
+ integrations: [
294
+ {
295
+ type: "facebook-pixel",
296
+ config: {
297
+ pixelId: "YOUR_PIXEL_ID",
298
+ eventMapping: {
299
+ "Added to Cart": "AddToCart",
300
+ "Payment Completed": "Purchase",
301
+ },
302
+ },
303
+ },
304
+ {
305
+ type: "tiktok-pixel",
306
+ config: {
307
+ pixelId: "YOUR_PIXEL_ID",
308
+ eventMapping: {
309
+ "Added to Cart": "AddToCart",
310
+ "Payment Completed": "Purchase",
311
+ },
312
+ },
313
+ },
314
+ {
315
+ type: "pinterest-tag",
316
+ config: {
317
+ tagId: "YOUR_TAG_ID",
318
+ eventMapping: {
319
+ "Added to Cart": "addtocart",
320
+ "Payment Completed": "checkout",
321
+ },
322
+ },
323
+ },
324
+ ],
325
+ });
326
+ ```
327
+
328
+ On the hosted script tag, pass pixel IDs and optional JSON event mappings:
329
+
330
+ ```html
331
+ <script
332
+ src="https://www.saasco.com/sdk/saasco-sdk.js"
333
+ data-projectId="YOUR-PROJECT-ID"
334
+ data-facebook-pixel-id="YOUR_PIXEL_ID"
335
+ data-facebook-pixel-event-mapping='{"Added to Cart":"AddToCart"}'
336
+ data-tiktok-pixel-id="YOUR_PIXEL_ID"
337
+ data-pinterest-tag-id="YOUR_TAG_ID"
338
+ ></script>
339
+ ```
340
+
341
+ Integrations only initialize when analytics `enabled` is `true`.
342
+
343
+ ## Social Proof Widget
344
+
345
+ Social proof notifications ("Someone from Berlin just signed up 2 hours ago") render from the `track` events you already send. There is no separate embed.
346
+
347
+ The widget is **available wherever the analytics SDK runs**. On `init()` the SDK does a cheap widget-payload check and lazily injects `saasco-social-proof-loader.js` only when the project has Social Proof enabled in the dashboard. Pages where it's off never download the loader, and the dashboard toggle controls every install (CDN and npm) uniformly.
348
+
349
+ You author templates in **Social Proof → Templates**. No client-side registration.
350
+
351
+ `baseUrl` / `scriptUrl` are only needed for same-origin, proxied, or self-hosted installs. CDN tags derive both from the analytics script `src`.
352
+
353
+ ### Standalone loader
354
+
355
+ If you don't use the analytics SDK, drop the loader in directly:
356
+
357
+ ```html
358
+ <script
359
+ src="https://www.saasco.com/sdk/saasco-social-proof-loader.js"
360
+ data-projectId="YOUR_PROJECT_ID"
361
+ data-base-url="https://www.saasco.com"
362
+ async
363
+ ></script>
364
+ ```
365
+
366
+ ### QA
367
+
368
+ Append these query params on any page that loads the SDK:
369
+
370
+ | Param | Effect |
371
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
372
+ | `?saasco_notifications=test` | Renders one sample notification from the first template (placeholder values, no tracking, no caps). Works even while the dashboard toggle is off. |
373
+ | `?saasco_notifications=reset` | Clears shown-ids, session count, dismissal, and the widget-payload cache, then runs the engine. |
374
+
375
+ ## Use the hosted SDK
376
+
377
+ To use the hosted JS SDK, add the following script just before `</body>`.
378
+ Common constructor options can be passed as `data-*` attributes (see the table). Nested fields such as `autoPageTracking.trackQueryParams` / `trackHash` and `debugVerbose` are npm/constructor-only — or use `?saasco-debug-verbose=true` for verbose logs.
379
+
380
+ ```html
381
+ <script
382
+ src="https://www.saasco.com/sdk/saasco-sdk.js"
383
+ data-projectId="YOUR-PROJECT-ID"
384
+ ></script>
385
+ ```
386
+
387
+ ```html
388
+ <script
389
+ src="https://www.saasco.com/sdk/saasco-sdk.js"
390
+ data-projectId="YOUR-PROJECT-ID"
391
+ data-debug="true"
392
+ data-enabled="true"
393
+ data-autoPageTracking="true"
394
+ ></script>
395
+ ```
396
+
397
+ | Attribute | Default | Description |
398
+ | ----------------------------------- | ------------------ | ------------------------------------------------------- |
399
+ | `data-projectId` | required | Project ID. |
400
+ | `data-debug` | `false` | `"true"` to log events. |
401
+ | `data-enabled` | `true` | `"false"` to disable ingest. |
402
+ | `data-autoPageTracking` | `true` | `"false"` to disable automatic page views. |
403
+ | `data-proxy` | Saasco ingest | Override the ingest host. |
404
+ | `data-placeholder` | dashboard branding | Chat composer placeholder. |
405
+ | `data-base-url` | script origin | Origin of the Saasco app (support + social-proof APIs). |
406
+ | `data-facebook-pixel-id` | — | Facebook Pixel ID. |
407
+ | `data-facebook-pixel-event-mapping` | `{}` | JSON map of Saasco event → Facebook standard event. |
408
+ | `data-tiktok-pixel-id` | — | TikTok Pixel ID. |
409
+ | `data-tiktok-pixel-event-mapping` | `{}` | JSON event map. |
410
+ | `data-pinterest-tag-id` | — | Pinterest Tag ID. |
411
+ | `data-pinterest-tag-event-mapping` | `{}` | JSON event map. |
412
+
413
+ The hosted script calls `init()` for you. Then:
414
+
415
+ ```js
416
+ saasco.track("Searched Movies");
417
+ ```
418
+
419
+ For type safety in a TypeScript project, add this to an `index.d.ts`:
420
+
421
+ ```ts
422
+ import { Saasco } from "saasco-sdk";
423
+
424
+ declare global {
425
+ interface Window {
426
+ saasco?: Saasco;
427
+ }
428
+ }
429
+ ```
430
+
431
+ ## Support Widget
432
+
433
+ The same `saasco-sdk` package ships an embeddable AI support widget.
434
+ Customers talk to a streaming AI support agent that can call tools you configure
435
+ in the dashboard; every message is persisted into your Saasco support inbox, and
436
+ your team can take the conversation over at any time.
437
+
438
+ The widget is **available wherever the analytics SDK runs** and is gated by
439
+ the dashboard toggle (**Support → Settings → Widget**). It runs as a
440
+ **cross-origin iframe** hosted on Saasco (Intercom-style): on `init()` the SDK
441
+ does a cheap `{ enabled }` check and lazily injects a lean host **loader**
442
+ (`saasco-support-loader.js`, no React) only when the project has Support
443
+ enabled. The loader injects an `iframe src="{baseUrl}/embed/support"` and
444
+ bridges it over `postMessage`. All of React and the widget UI live inside that
445
+ iframe on the Saasco origin, so pages where the widget is off never download
446
+ it and the host page's CSS can never reach the widget.
447
+
448
+ ### Before you embed
449
+
450
+ Add your site's origin (e.g. `https://example.com`) to the widget allowlist in
451
+ **Support → Settings → Security**. Cross-origin requests from unlisted
452
+ origins are rejected. The allowlist gates _embedding_ only — each conversation
453
+ is additionally protected by an opaque per-conversation session token, which is
454
+ the real auth.
455
+
456
+ ### Install — script tag (one tag, shared projectId)
457
+
458
+ The same analytics `saasco-sdk.js` tag is enough. The analytics loader
459
+ lazy-injects `saasco-support-loader.js` from the same `/sdk/` origin when the
460
+ dashboard toggle is on, and reuses the `data-projectId` you already provide —
461
+ you never declare the project id twice:
462
+
463
+ ```html
464
+ <script
465
+ src="https://www.saasco.com/sdk/saasco-sdk.js"
466
+ data-projectId="YOUR_PROJECT_ID"
467
+ async
468
+ ></script>
469
+ ```
470
+
471
+ The agent's tools are configured in the dashboard (**Support → Tools**) and
472
+ loaded automatically — no code registration. Optionally push host
473
+ state and drive the widget imperatively:
474
+
475
+ ```js
476
+ window.SaascoSupport.setStateSnapshot({ page: location.pathname });
477
+ window.SaascoSupport.open();
478
+ ```
479
+
480
+ `window.SaascoSupport` API:
481
+
482
+ - `identify({ distinctId, email, traits })` — unsigned CRM attributes. Extra
483
+ properties go in a nested `traits` object (`{ plan: "pro" }`), not on the
484
+ root. The full scalar trait payload (plus `saasco.track` browser / session
485
+ context) flows into the agent's **live context**, so tool parameters can bind
486
+ any identify trait or track key automatically. `saasco.identify(...)` already
487
+ forwards identity here. Call this yourself if you only load the standalone
488
+ support loader.
489
+ - `boot({ userJwt, distinctId, email })` / `update({ userJwt })` — supply a
490
+ **signed identity JWT** (see [Verified identity](#verified-identity-jwt));
491
+ layers on top of `identify()`, it does not replace it.
492
+ - `shutdown()` — on host logout: clears the signed identity + session.
493
+ - `setStateSnapshot(value)` — **additional** host-only keys layered on top of the
494
+ live context (identify traits / track context flow through automatically); also
495
+ appended to the agent system prompt. Host keys win on conflict.
496
+ - `registerTool({ name, execute })` — register a **client tool** the agent can
497
+ call; it runs in your page (see [Tools](#tools)).
498
+ - `open()` / `close()`.
499
+
500
+ The analytics script injects the loader asynchronously, so `window.SaascoSupport`
501
+ appears a tick after your inline script runs. Wait for it before calling the API:
502
+
503
+ ```js
504
+ function whenSupportReady(callback) {
505
+ if (window.SaascoSupport) {
506
+ callback();
507
+ return;
508
+ }
509
+ const interval = setInterval(() => {
510
+ if (window.SaascoSupport) {
511
+ clearInterval(interval);
512
+ callback();
513
+ }
514
+ }, 50);
515
+ }
516
+
517
+ whenSupportReady(() => {
518
+ window.SaascoSupport.setStateSnapshot({ page: location.pathname });
519
+ });
520
+ ```
521
+
522
+ Optional passthrough attributes on the analytics tag: `data-placeholder` and
523
+ `data-base-url`. (Workspace branding — name, greeting, avatar — is configured in
524
+ the dashboard and loaded from the public settings endpoint, not passed here.)
525
+
526
+ `baseUrl` / `scriptUrl` are only needed for same-origin, proxied, or self-hosted
527
+ installs. CDN tags derive both from the analytics script `src`. npm/bundled
528
+ installs fall back to the public Saasco CDN.
529
+
530
+ ### Install — standalone loader (any framework)
531
+
532
+ If you don't use the analytics SDK, drop the loader in directly. It injects the
533
+ cross-origin embed iframe and exposes `window.SaascoSupport`:
534
+
535
+ ```html
536
+ <script
537
+ src="https://www.saasco.com/sdk/saasco-support-loader.js"
538
+ data-projectId="YOUR_PROJECT_ID"
539
+ data-base-url="https://www.saasco.com"
540
+ async
541
+ ></script>
542
+ ```
543
+
544
+ > The widget UI runs on the Saasco origin, so the previously-exported React
545
+ > `SupportWidget` component is gone — host pages integrate via the loader +
546
+ > `window.SaascoSupport`, never by rendering the widget themselves. (The
547
+ > iframe app entry `SupportWidgetInner` is still exported from
548
+ > `saasco-sdk/support` for advanced self-hosting of the embed page only.)
549
+
550
+ ### Verified identity (JWT)
551
+
552
+ `identify()` forwards **unsigned** attributes — fine for attaching a conversation
553
+ to a contact, but not proof of who the visitor is. To let the agent trust a
554
+ `distinctId`/`email` as a data-connector identifier, sign a short-lived JWT on
555
+ **your backend** with your project's **messenger secret** (generate it in
556
+ **Support → Settings → Security**) and pass it via `boot`/`update`:
557
+
558
+ ```js
559
+ // your backend (after auth), HS256 with the messenger secret:
560
+ // jwt.sign({ user_id: user.id, email: user.email, exp }, MESSENGER_SECRET)
561
+ window.SaascoSupport.boot({ userJwt }); // + optional distinctId/email
562
+ window.SaascoSupport.shutdown(); // on logout
563
+ ```
564
+
565
+ Never ship the secret in client code. With **Require verified identity** on, a
566
+ chat that _claims_ an identity without a valid JWT is rejected (anonymous chat is
567
+ unaffected). For signed-in users, the open conversation also resumes server-side
568
+ from the verified `user_id` — no reliance on (third-party-partitioned) iframe
569
+ storage, so it works across reloads and devices.
570
+
571
+ ### Tools
572
+
573
+ Tools are configured in the dashboard (**Support → Tools**). The
574
+ agent builds its tool list server-side on every request, so a tampered client
575
+ request can't add or alter tools. Two kinds:
576
+
577
+ - **Server tool calls** (`execution: server`): a REST call — name,
578
+ description, JSON Schema arguments, HTTP method, an `https://` endpoint on a
579
+ public host, and a mapping of each argument to path, query, or body. The agent
580
+ runs it itself, proxying the request and streaming the result inline. Follow
581
+ Intercom's data-connector guidance: trusted identifiers, no freeform sensitive
582
+ input, server-side validation on your API.
583
+ - **Host tools** (`execution: host`): author a stub (name + description +
584
+ arguments) in the dashboard so the agent advertises it. The call is streamed
585
+ back to the widget and round-tripped over the postMessage bridge to your page
586
+ (15s timeout). A `registerTool({ name, execute })` handler wins and runs in the
587
+ **host realm** (free to `fetch(..., { credentials: "include" })`, mutate
588
+ in-page state, etc.). If you set an endpoint instead, it must be a **relative
589
+ same-origin path** (`/api/…`) — the loader fetches it against the host page
590
+ with cookies. Absolute `https://` URLs are rejected for host tools.
591
+ - **Destructive tools** (`destructive: true`): always host-executed, paused on an
592
+ Approve/Reject card; a decline is reported to the model as
593
+ `{ ok: false, error: "User declined" }`.
594
+ - Context parameters are filled at call time — not by the model — from the
595
+ visitor's **live context**: the full scalar payload of `saasco.identify(...)`
596
+ (identify traits like `firstName`, `$country`), the `saasco.track` browser /
597
+ session context (`$utmSource`, `anonymousId`, `sessionId`, …), the verified
598
+ identity (`distinctId` / `email`) and any host `setStateSnapshot` keys. So a
599
+ tool parameter bound to `firstName` resolves automatically after
600
+ `saasco.identify(...)` — no need to mirror traits into `setStateSnapshot`. Each
601
+ parameter can instead resolve from the **CRM** record; the dashboard picker
602
+ chooses Live vs CRM per parameter.
603
+ - The model's args are not validated server-side, so your endpoint should
604
+ validate its own inputs. Display names are 1–128 characters; the agent id is
605
+ a normalized `[a-z0-9_-]{1,64}` (punctuation collapsed, may start with a
606
+ digit). Active tools need a non-empty description.
607
+
608
+ Platform tools (server-side; not registered in your page):
609
+
610
+ - `close_conversation` — always available; never shown as a pill.
611
+ - `request_human_handoff` — offered when the project has handoff notifications
612
+ configured; shown as a confirmation card, not a tool pill.
613
+ - `understand_product` — offered when the project has a help center; searches
614
+ the knowledge base to answer the user's question. The agent is instructed never to mention it to the
615
+ customer.
616
+ - `report_knowledge_gap` — offered only when a help center exists; never shown
617
+ as a pill.
618
+
619
+ ### Behavior
620
+
621
+ - The widget opens on a Home / Messages home screen; canned prompts come from
622
+ the dashboard.
623
+ - New conversations ask for an email before the first message unless
624
+ `identify` / a verified JWT already supplied one.
625
+ - Messages are limited to 5000 characters.
626
+ - Visitors can attach up to 5 images per message (6 MB each).
627
+ - The conversation (id + session token) and captured email are stored in the
628
+ **iframe's** `localStorage` (Saasco origin) under
629
+ `saasco-chat-session:<projectId>` and `saasco-chat-email:<projectId>`. They
630
+ are not on the host page — Safari / partitioned storage is why verified JWT
631
+ resume exists.
632
+ - Human replies from the support inbox are polled every 5 seconds. While a
633
+ teammate is composing, the widget shows "{name} is typing…" (or "The team is
634
+ typing…" when more than one person is).
635
+ - **Human takeover:** once a teammate replies (or toggles the AI off in the
636
+ inbox), the agent stops responding — the widget shows "Waiting for a
637
+ teammate…" until someone replies, then "{name} has joined the conversation"
638
+ above the first team message. Toggling the AI back on resumes streaming.
639
+
640
+ ## Reserved Properties
641
+
642
+ Saasco has reserved some properties that have semantic meanings for contacts and will handle them in special ways.
643
+ For example, Saasco always expects `email` to be a string of the user's email address, this is important for when sending emails using the marketing app.
644
+ You must use the exact property keys listed below.
645
+
646
+ ### Reserved Contact Properties
647
+
648
+ These properties have semantic meaning and are used by features like email marketing. Use the exact key names below in your identify calls.
649
+
650
+ | Property | Type | Description |
651
+ | ----------- | ------ | ---------------------------------------------------------------------------- |
652
+ | displayName | String | The preferred display name, defaults to "firstName lastName" |
653
+ | email | String | Email address of the user |
654
+ | firstName | String | First name of the user |
655
+ | lastName | String | Last name of the user |
656
+ | name | String | Full name of the user |
657
+ | title | String | Job title or position (e.g. "VP of Engineering") |
658
+ | phone | String | Phone number of the user |
659
+ | avatar | String | URL to an avatar image for the user |
660
+ | gender | String | Gender of the user |
661
+ | age | Number | Age of the user |
662
+ | birthday | Date | The users date of birth |
663
+ | website | String | Personal or company website |
664
+ | username | String | Username, should be unique per user (like Twitter or GitHub usernames) |
665
+ | description | String | Bio or about text |
666
+ | createdAt | Date | Date the user account was first created. We recommend ISO-8601 date strings. |
667
+ | id | String | Reserved; coerced to a string. Prefer your Distinct ID in `identify()`. |
668
+
669
+ ### Default Contact Properties
670
+
671
+ Our SDKs automatically collect certain properties on every user profile.
672
+ Keys that start with `$` are system-managed — do not set them in `identify()`. Most are read-only; some geo fields can be edited in the CRM.
673
+
674
+ | Property | Display Name | Description |
675
+ | ----------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
676
+ | $id | Distinct ID | Unique identifier from your application, coerced to a string |
677
+ | $lastSeen | Last Seen | Last activity timestamp |
678
+ | $lastIdentifiedAt | Last Identified | Timestamp of the most recent identify() call |
679
+ | $processedAt | Processed At | Timestamp when the most recent event was processed |
680
+ | $subscribed | Subscribed | Email subscription status |
681
+ | $unsubscribed | Unsubscribed | Email unsubscription status |
682
+ | $unsubscribeReason | Unsubscribe Reason | Reason the user was unsubscribed (e.g. complained, bounced) |
683
+ | $unsubscribeSource | Unsubscribe Source | Source of the unsubscribe action |
684
+ | $emailValidated | Email Validated | Whether the email has been validated |
685
+ | $lastEmailValidatedAt | Last Email Validated At | Timestamp of the last email validation |
686
+ | $emailValidationResult | Email Validation Result | Result from email validation (Invalid, Risky, Safe to Send, or Unknown) |
687
+ | $emailScore | Email Score | Engagement score derived from open/click history with time decay |
688
+ | $os | Operating System | OS from the most recent session |
689
+ | $browser | Browser | Browser from the most recent session |
690
+ | $browserVersion | Browser Version | Browser version from the most recent session |
691
+ | $device | Device | Device type from the most recent session (Desktop, Mobile, Tablet) |
692
+ | $screenHeight | Screen Height | Height of the device screen in pixels |
693
+ | $screenWidth | Screen Width | Width of the device screen in pixels |
694
+ | $screenDpi | Screen DPI | Pixel density of the device screen |
695
+ | $userAgent | User Agent | Browser user agent from the most recent session |
696
+ | $ip | IP Address | IP address from the most recent session |
697
+ | $timezone | Timezone | Timezone from the most recent session (e.g. America/New_York) |
698
+ | $city | City | City from the most recent session |
699
+ | $country | Country | Country from the most recent session |
700
+ | $countryCode | Country Code | Country code from the most recent session |
701
+ | $continent | Continent | Continent from the most recent session |
702
+ | $region | Region | State or province from the most recent session |
703
+ | $locale | Locale | Preferred language of the user |
704
+ | $latitude | Latitude | Latitude from the most recent session |
705
+ | $longitude | Longitude | Longitude from the most recent session |
706
+ | $referrer | Last Touch Referrer | Referring URL from the most recent session. Empty when there is no referrer; the CRM may display this as "direct" |
707
+ | $referringDomain | Last Touch Referring Domain | Referring domain from the most recent session. Empty when there is no referrer; the CRM may display this as "direct" |
708
+ | $initialReferrer | Initial Referrer | Referring URL from the users first tracked session. Empty when there is no referrer; the CRM may display this as "direct" |
709
+ | $initialReferringDomain | Initial Referring Domain | Referring domain from the users first tracked session. Empty when there is no referrer; the CRM may display this as "direct" |
710
+ | $utmSource | Last Touch UTM Source | UTM source from the most recent session |
711
+ | $utmMedium | Last Touch UTM Medium | UTM medium from the most recent session |
712
+ | $utmCampaign | Last Touch UTM Campaign | UTM campaign from the most recent session |
713
+ | $utmTerm | Last Touch UTM Term | UTM term from the most recent session |
714
+ | $utmContent | Last Touch UTM Content | UTM content from the most recent session |
715
+ | $initialUtmSource | Initial UTM Source | UTM source from the users first tracked session |
716
+ | $initialUtmMedium | Initial UTM Medium | UTM medium from the users first tracked session |
717
+ | $initialUtmCampaign | Initial UTM Campaign | UTM campaign from the users first tracked session |
718
+ | $initialUtmTerm | Initial UTM Term | UTM term from the users first tracked session |
719
+ | $initialUtmContent | Initial UTM Content | UTM content from the users first tracked session |
720
+
721
+ ### Reserved Integration Properties
722
+
723
+ | Property | Display Name | Description |
724
+ | ----------------- | ------------------ | ------------------------------------------------ |
725
+ | $stripeCustomerId | Stripe Customer ID | The Stripe customer ID associated with this user |
726
+
727
+ ### Reserved event properties
728
+
729
+ Properties used to calculate revenue for different traffic sources and LTV for users.
730
+
731
+ | PROPERTY | TYPE | DESCRIPTION |
732
+ | -------- | ------ | ----------------------------------------------------------------------------------------- |
733
+ | revenue | Number | Amount of revenue an event resulted in. This should be a decimal value |
734
+ | currency | String | Currency of the revenue an event resulted in. This should be sent in the ISO 4217 format. |
735
+ | value | Number | An abstract numerical value used internally to score events, such as lead scoring. |
736
+
737
+ ### Default Event Properties
738
+
739
+ The SDK attaches browser context to every client-side event. Keys begin with `$`. Avoid leading `$` in your own properties to avoid conflicts. Geo, device, OS, browser, and bot fields are added by the ingest pipeline from IP / user agent. UTM and ad-id fields are **session-sticky**: the first non-empty value of the session is kept (later navigations do not overwrite). Referrer is the last **cross-domain** referrer of the session.
740
+
741
+ | Property | Display Name | Description |
742
+ | ------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
743
+ | $href | Current URL | The URL of the page on which the event was tracked. |
744
+ | $pathname | Pathname | The path of the page on which the event was tracked. |
745
+ | $title | Page Title | The title of the page on which the event was tracked. |
746
+ | $location | Location | Timezone-derived location hint (e.g. country from `Intl`). |
747
+ | $locale | Locale | The preferred language of the user. |
748
+ | $userAgent | User Agent | The user agent string of the browser. |
749
+ | $screenHeight | Screen Height | The height of the device screen in pixels |
750
+ | $screenWidth | Screen Width | The width of the device screen in pixels |
751
+ | $screenDPI | Screen DPI | Pixel density of the device screen. |
752
+ | $referrer | Last Touch Referrer | Last cross-domain `document.referrer` this session. The CRM may display this as "direct" |
753
+ | $referringDomain | Last Touch Referring Domain | Hostname of that referrer. The CRM may display this as "direct" |
754
+ | $utmSource | UTM Source | First `utm_source` of the session |
755
+ | $utmMedium | UTM Medium | First `utm_medium` of the session |
756
+ | $utmCampaign | UTM Campaign | First `utm_campaign` of the session |
757
+ | $utmTerm | UTM Term | First `utm_term` of the session |
758
+ | $utmContent | UTM Content | First `utm_content` of the session |
759
+ | $utmId | UTM ID | First `utm_id` of the session |
760
+ | $utmCampaignId | UTM Campaign ID | First `utm_campaign_id` of the session |
761
+ | $utmSourcePlatform | UTM Source Platform | First `utm_source_platform` of the session |
762
+ | $utmCreativeFormat | UTM Creative Format | First `utm_creative_format` of the session |
763
+ | $utmMarketingTactic | UTM Marketing Tactic | First `utm_marketing_tactic` of the session |
764
+ | $utmAdId | Ad ID | First `utm_ad_id` or network-specific ad id (`fbadid`, `gadid`, `ttadid`, …) |
765
+ | $utmAdSource | Ad Source | First `utm_ad_source`, or the network inferred from the ad-id param |
766
+ | $os | Operating System | Parsed from the user agent (ingest). |
767
+ | $browser | Browser | Parsed from the user agent (ingest). |
768
+ | $browserVersion | Browser Version | Parsed from the user agent (ingest). |
769
+ | $device | Device Type | Parsed from the user agent. eg `Mobile`, `Tablet`, `Desktop` |
770
+ | $ip | IP Address | Client IP from the ingest request. |
771
+ | $city | City | The city of the user parsed from the IP. |
772
+ | $country | Country | ISO country code from the IP (ingest writes `$country`, not `$countryCode`). |
773
+ | $continent | Continent | Continent from the IP. |
774
+ | $region | Region | State or province from the IP. |
775
+ | $latitude | Latitude | Latitude of the user's IP location. |
776
+ | $longitude | Longitude | Longitude of the user's IP location. |
777
+ | $timezone | Timezone | Timezone of the user parsed from the IP. |
778
+ | $isBot | Bot | `true` when ingest classifies the user agent as a crawler. |
779
+
780
+ ### Reserved Events with Optional Properties
781
+
782
+ > The event schema described below is not yet implemented in the UI. However, if you wish to future-proof your implementation, you can start using this schema in anticipation of its integration.
783
+
784
+ Reserved events are common events that happen during the lifecycle of a user, with optional properties to capture more detailed information, including revenue data where appropriate.
785
+
786
+ - [Signed Up](#signed-up)
787
+ - [Signed In](#signed-in)
788
+ - [Signed Out](#signed-out)
789
+ - [Trial Started](#trial-started)
790
+ - [Trial Ended](#trial-ended)
791
+ - [Payment Completed](#payment-completed)
792
+ - [Subscription Started](#subscription-started)
793
+ - [Subscription Cancelled](#subscription-cancelled)
794
+ - [Subscription Upgraded](#subscription-upgraded)
795
+ - [Subscription Downgraded](#subscription-downgraded)
796
+
797
+ #### Signed Up
798
+
799
+ Event triggered when a user signs up.
800
+
801
+ | Property | Description |
802
+ | -------- | -------------------------------------- |
803
+ | `source` | How the user found your site. |
804
+ | `value` | Track an estimated value of the signup |
805
+
806
+ Example:
807
+
808
+ ```js
809
+ saasco.track("Signed Up", {
810
+ source: "Referral by friend",
811
+ });
812
+ ```
813
+
814
+ #### Signed In
815
+
816
+ Event triggered when a user signs in.
817
+
818
+ | Property | Description |
819
+ | ---------- | --------------------------------------------------------------- |
820
+ | `provider` | The provider used for signing in (e.g., email, github, google). |
821
+
822
+ Example:
823
+
824
+ ```js
825
+ saasco.track("Signed In", {
826
+ provider: "email",
827
+ });
828
+ ```
829
+
830
+ When tracking Signed In events call `identify('user_id', { email: '...' })` before the event to connect to the user.
831
+
832
+ #### Signed Out
833
+
834
+ Event triggered when a user signs out.
835
+ No optional properties.
836
+
837
+ Example:
838
+
839
+ ```js
840
+ saasco.track("Signed Out");
841
+ ```
842
+
843
+ When tracking Signed Out events call `saasco.logout()` _after_ tracking the event to reset the session (or `identify(null)` if you only need to clear the user id).
844
+
845
+ #### Trial Started
846
+
847
+ Event triggered when a user starts a trial.
848
+
849
+ | Property | Description |
850
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
851
+ | `duration` | The duration of the trial in days |
852
+ | `type` | `optIn` when the user didn't provide a card, or `optOut` when they did and billing starts automatically at the end of the trial |
853
+
854
+ Example:
855
+
856
+ ```js
857
+ saasco.track("Trial Started", { duration: 14, type: "optOut" });
858
+ ```
859
+
860
+ #### Trial Ended
861
+
862
+ Event triggered when a user's trial ends.
863
+
864
+ | Property | Description |
865
+ | ----------------- | -------------------------------------------------------------------------------- |
866
+ | `daysLeftInTrial` | If a user manually upgrades before the end of the trial you can record this here |
867
+
868
+ Example:
869
+
870
+ ```js
871
+ saasco.track("Trial Ended", { daysLeftInTrial: 4 });
872
+ ```
873
+
874
+ #### Payment Completed
875
+
876
+ Event triggered when a payment is completed.
877
+ This may be called on the server side after a confirmation webhook.
878
+ This is helpful for recording ongoing subscription payments.
879
+
880
+ | Property | Description |
881
+ | ---------- | --------------------------------------------------- |
882
+ | `revenue` | The initial payment amount. |
883
+ | `currency` | The currency of the payment, otherwise assumed USD. |
884
+
885
+ Example:
886
+
887
+ ```js
888
+ saasco.track("Payment Completed", { revenue: 29.99, currency: "GBP" });
889
+ ```
890
+
891
+ #### Subscription Started
892
+
893
+ Event triggered when a user starts a subscription.
894
+ If you have integrated with Stripe for revenue tracking or are doing server side revenue tracking with webhooks you should skip the `revenue` and `currency` properties to prevent duplicate revenue tracking.
895
+
896
+ | Property | Description |
897
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
898
+ | `plan` | The name or ID of the subscription plan. |
899
+ | `revenue` | The initial payment amount. Exclude if triggering `Payment Completed` to avoid double counting. |
900
+ | `currency` | The currency of the payment, otherwise assumed USD. Exclude if triggering `Payment Completed` to avoid double counting. |
901
+
902
+ Example:
903
+
904
+ ```js
905
+ saasco.track("Subscription Started", {
906
+ plan: "Monthly",
907
+ });
908
+ ```
909
+
910
+ #### Subscription Cancelled
911
+
912
+ Event triggered when a subscription is cancelled.
913
+
914
+ | Property | Description |
915
+ | -------- | -------------------------------------------------------------------------------------------------- |
916
+ | `reason` | The reason for cancellation. This can be used in the feedback and analysis of cancellation reasons |
917
+
918
+ Example:
919
+
920
+ ```js
921
+ saasco.track("Subscription Cancelled", {
922
+ reason: "Not using it any more",
923
+ });
924
+ ```
925
+
926
+ #### Subscription Upgraded
927
+
928
+ Event triggered when a subscription is upgraded.
929
+
930
+ | Property | Description |
931
+ | ----------------- | -------------------------------------------------------------------------- |
932
+ | `fromPlan` | The previous plan. |
933
+ | `toPlan` | The new plan. |
934
+ | `previousRevenue` | The revenue from the previous plan. |
935
+ | `newRevenue` | The revenue from the new plan. |
936
+ | `revenue` | If the customer is charged immediately then include a revenue number here |
937
+ | `currency` | The currency of the payment, assumed to be USD unless otherwise specified. |
938
+
939
+ Example:
940
+
941
+ ```js
942
+ saasco.track("Subscription Upgraded", {
943
+ fromPlan: "Monthly",
944
+ toPlan: "Annual",
945
+ previousRevenue: 29,
946
+ newRevenue: 129,
947
+ });
948
+ ```
949
+
950
+ #### Subscription Downgraded
951
+
952
+ Event triggered when a subscription is downgraded.
953
+
954
+ | Property | Description |
955
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
956
+ | `fromPlan` | The previous plan. |
957
+ | `toPlan` | The new plan. |
958
+ | `previousRevenue` | The revenue from the previous plan. |
959
+ | `newRevenue` | The revenue from the new plan. |
960
+ | `revenue` | If the customer is refunded immediately then include a revenue number here, in the case of a downgrade this will be a negative value. Only include the revenue here if it's not tracked on your backend or with a webhook |
961
+ | `currency` | The currency of the payment, assumed to be USD unless otherwise specified. |
962
+
963
+ Example:
964
+
965
+ ```js
966
+ saasco.track("Subscription Downgraded", {
967
+ fromPlan: "Annual",
968
+ toPlan: "Monthly",
969
+ previousRevenue: 129,
970
+ newRevenue: 29,
971
+ revenue: -36,
972
+ currency: "USD",
973
+ });
974
+ ```