Widget Integration
The UserTold.ai widget runs inside your product. Participants open a floating launcher, answer the intake, and start an interview in context.
Basic Embed
Add one line to your page:
<script
async
src="https://usertold.ai/v1/widget.js"
data-project-key="ut_pub_YOUR_KEY"
></script>
Install this Project script once across the site. Visibility selects one active Study for the current pathname and widget language; that Study's Invitation defines the launcher. One Project can contain many active Studies.
Without limits, an active Study is eligible on all pages and all supported widget languages; enabled: false opts it out. Exclusions win, followed by page and language specificity, higher priority, and lower Project order. A final tie fails closed, and an unavailable winner does not fall back to a broader Study.
A missing Invitation uses the built-in responsive launcher and localized label. The loader waits for the host page's load event before scheduling the versioned widget bundle. The winning Study's Invitation then appears in its responsive corner.
The stable loader is served from https://usertold.ai. It normally loads immutable, content-hashed runtime assets from https://assets.usertold.ai; identical same-origin copies are available only as a compatibility fallback. Keep these public origins separate from the dashboard origin (https://app.usertold.ai).
Performance
The widget is designed to stay off the host page's critical render path:
- Two-stage loading — the stable loader schedules the versioned core bundle after
window.load, using a background task API when the browser provides one. - Connection warm-up — the loader adds a
preconnecthint when scheduling begins. - Data saver — when a visitor has data-saver enabled (
navigator.connection.saveData), the loader skips loading the widget entirely out of respect for their choice. - Failure isolation — a slow or unavailable widget bundle does not block the host page; the launcher remains absent until initialization succeeds.
Invitation copy, managed image, exact reward, colors, and placement belong to the Study; Visibility owns automatic routing. A direct_link Invitation returns:
https://customer.example/relevant-page?ut_research=<opaque-ref>
The opaque reference has no PII. The widget sends only ut_research, pathname, and language—not the full query. It selects that exact active Study; invalid, revoked, wrong-Project, or unavailable links fail closed without fallback. Arrival starts nothing before Start. Changing modes revokes it; re-enabling creates a new link. There is no email, CRM, or recipient tracking.
Options
| Option | Type | Default | Description |
|---|---|---|---|
projectKey | string | — | Required. Public project key (ut_pub_...) |
brandColor | string | #c46f4f | Advanced host color override. |
theme.light.bg / theme.light.fg | string | — | Advanced light-theme override. |
theme.dark.bg / theme.dark.fg | string | — | Advanced dark-theme override. |
position | string | bottom-right | Widget position: bottom-right or bottom-left |
defaultDocked | 'left' | 'right' | false | false | Collapse the launcher into a side dock on initial render. |
launcherText | string | localized | Advanced host label override. |
voiceAssistant | boolean | false | Enable OpenAI Realtime voice for talk segments. Requires a positive prepaid balance and either Managed AI or a usable project OpenAI key. |
voice | string | marin | AI interviewer voice. |
voiceMode | 'auto' | 'auto' | Server-VAD turn detection. |
onComplete | function | — | Runs when the completion panel closes or the widget instance is destroyed. |
The project public key, matched Study handle, Invitation, and Visibility targeting are public presentation configuration, not secrets. allowed_origins is a browser collection boundary checked against the real HTTP Origin; it is not authentication and does not make targeting private or defend a server client that forges an Origin header. Keep secret project credentials out of the embed.
Participant Flow
- Launcher — The winning Study's saved Invitation, or the exact direct-link Invitation selected by
ut_research. - Intake — Optional qualification before the linked Study.
- Permissions — Requests microphone access everywhere. On desktop browsers with screen capture support, it also requests screen sharing. On mobile or unsupported devices, interviews continue with audio and in-page events only.
- Interview — Evidence-first conversation. The study script moves through
talk, scriptedspeak, and silentobservesegments so participants can complete tasks and then debrief from captured context. - Complete — Thank-you message with optional incentive information.
If a participant doesn't qualify, they see a polite disqualification message and can close the widget.
Recording Integrity and Recovery
Recording is append-only and resumable. With browser storage, pending parts persist and each stream stays ordered. Parts are at most 1 MiB, carry a SHA-256 digest, and use conditional R2 writes. Identical retries are idempotent; conflicting bytes cannot overwrite a part. The server returns the next sequence and resume position.
Workflows coordinate media preparation, transcription, and Evidence extraction with deterministic run identities. An atomic claim prevents overlapping analysis. Stale transcripts remain non-authoritative, and uncertain dispatch stays visible. Recovery starts only after a part is persisted, so abrupt device or browser loss can create gaps. SHA-256 checks transfer integrity, not media authenticity.
Programmatic Control
window.UserTold queues calls made before the core bundle boots and replays them in order:
| Method | What it does |
|---|---|
init(config?) | Initialize the widget. Merges config over window.UserToldSettings and script data-* attributes. |
identify(userId, traits?) | Attach a host-side user identifier and free-form traits to the current widget interview for later correlation. |
on(event, handler) / off(event, handler) | Subscribe / unsubscribe to widget lifecycle events. |
initialized | Boolean — true once the widget has finished bootstrapping. |
Identify Participants
UserTold.identify('user_123', {
email: 'user@example.com',
name: 'Jane Doe',
plan: 'pro',
// Any custom traits
});
Styling
The widget renders inside a Shadow DOM to isolate it from host-page CSS. Configure the launcher label, light/dark brand colors, and responsive corner in the Study's Widget editor. The widget follows prefers-color-scheme; the saved mobile corner applies at 768px and below.
For host-specific geometry or deeper theming, set stable custom properties on the widget host. These override the Study without reaching into Shadow DOM internals:
#usertold-widget-host {
--ut-brand-color: #2563eb;
--ut-brand-foreground: #ffffff;
--ut-bg-light: #ffffff;
--ut-bg-dark: #111827;
--ut-fg-light: #111827;
--ut-fg-dark: #f9fafb;
--ut-widget-bottom-offset: calc(24px + env(safe-area-inset-bottom));
--ut-widget-edge-offset: calc(24px + env(safe-area-inset-right));
}
@media (max-width: 768px) {
#usertold-widget-host {
--ut-widget-bottom-offset: calc(72px + env(safe-area-inset-bottom));
--ut-widget-edge-offset: calc(16px + env(safe-area-inset-right));
}
}
Use --ut-widget-edge-offset for either configured corner. If left and right safe-area insets differ, choose the applicable inset in your own responsive host rule. JavaScript brandColor, theme, position, and launcherText remain advanced per-installation overrides.
Content Security Policy
The widget does not require style-src 'unsafe-inline', script-src blob:, or worker-src blob:. Its Shadow DOM uses a constructed stylesheet, and PCM playback loads a content-hashed AudioWorklet module from the public widget asset origin.
For a source-allowlist CSP, permit the widget origins in the directives your policy already defines:
script-src https://usertold.ai https://assets.usertold.ai
connect-src https://usertold.ai wss://usertold.ai https://assets.usertold.ai https://api.openai.com
If your script-src is nonce-based, put the request nonce on the stable loader tag. The loader copies it to the versioned core script, and the widget's stylesheet fallback uses the same nonce on browsers without constructed stylesheet support.
Integration diagnostics
Register the browser listener before the loader tag so it can also report a core script blocked by CSP:
window.addEventListener('usertold:integration-error', ({ detail }) => {
console.error('UserTold integration error', detail);
});
detail.code distinguishes core_asset_blocked, core_asset_unreachable, csp_blocked, availability_failed, session_connection_failed, and worklet_unavailable. The detail also includes the failure phase, whether retry is safe, and CSP directive/URL fields when available. The loaded API reports the runtime cases through UserTold.on('error', handler) as well.
The widget stays hidden while availability is unknown, retries transient availability errors in the background, and remains hidden when collection is intentionally inactive. If a started interview loses its connection, it keeps the panel visible with an accurate retry or end-interview action; it never presents a non-recording startup failure as an active interview.
Troubleshooting
| Issue | Solution |
|---|---|
| Widget doesn't appear | Check that projectKey is valid (ut_pub_...), at least one active Study has automatic placement enabled, and its Visibility matches this pathname/language. |
| Widget does not load for some visitors | Listen for the usertold:integration-error browser event and check its detail.code. The widget still loads when data saver is enabled; asset, CSP, availability, and session-start failures are reported separately. |
| Intake opens but nobody reaches an interview | Confirm the intake is active, has qualification rules that can pass, and is linked to an active study. |
| Microphone permission denied | The participant must grant microphone access. Check browser permission settings. |
| Screen sharing is unavailable on mobile | The widget automatically falls back to microphone and in-page event capture on mobile and other devices without usable screen capture. |
| "No active study linked" | The intake must have a linked study with status active. |
| Widget styles look broken | Check that CSP permits the stable loader and immutable asset origins. The widget does not need style-src 'unsafe-inline'. |
| Interview hangs | Check browser console for WebSocket errors. Verify the study is active, the participant granted permissions, and the project can run inference through Managed AI or your OpenAI key. |
| Interview completes but no evidence appears | Open the interview in the dashboard or use the CLI to inspect transcript, timeline, and processing status before reprocessing. |
See also
- Quickstart — end-to-end setup including embed step
- Studies — configure what the widget asks
- Security — BYOK custody, private edge caching, and off-provider backups
- CLI Reference — import, upload, inspect, and reprocess interviews