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 preconnect hint 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

OptionTypeDefaultDescription
projectKeystringRequired. Public project key (ut_pub_...)
brandColorstring#c46f4fAdvanced host color override.
theme.light.bg / theme.light.fgstringAdvanced light-theme override.
theme.dark.bg / theme.dark.fgstringAdvanced dark-theme override.
positionstringbottom-rightWidget position: bottom-right or bottom-left
defaultDocked'left' | 'right' | falsefalseCollapse the launcher into a side dock on initial render.
launcherTextstringlocalizedAdvanced host label override.
voiceAssistantbooleanfalseEnable OpenAI Realtime voice for talk segments. Requires a positive prepaid balance and either Managed AI or a usable project OpenAI key.
voicestringmarinAI interviewer voice.
voiceMode'auto''auto'Server-VAD turn detection.
onCompletefunctionRuns 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

  1. Launcher — The winning Study's saved Invitation, or the exact direct-link Invitation selected by ut_research.
  2. Intake — Optional qualification before the linked Study.
  3. 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.
  4. Interview — Evidence-first conversation. The study script moves through talk, scripted speak, and silent observe segments so participants can complete tasks and then debrief from captured context.
  5. 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:

MethodWhat 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.
initializedBoolean — 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

IssueSolution
Widget doesn't appearCheck 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 visitorsListen 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 interviewConfirm the intake is active, has qualification rules that can pass, and is linked to an active study.
Microphone permission deniedThe participant must grant microphone access. Check browser permission settings.
Screen sharing is unavailable on mobileThe 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 brokenCheck that CSP permits the stable loader and immutable asset origins. The widget does not need style-src 'unsafe-inline'.
Interview hangsCheck 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 appearsOpen 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