vTilt
Why vTiltHow It WorksFeaturesFAQDocs
Docs / Chat widget
Quick startEvent forwarding
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceGoogle Ads
Client setup
CursorClaude DesktopVS CodeCodex
Realtime
Debug ViewRealtime Dashboard
Integration guides
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
SDK
Browser SDK
InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdown

Documentation

vTilt
Quick startEvent forwarding

MCP server

Realtime

Debug ViewRealtime Dashboard

Integration guides

SDK

InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
DocsBrowser SDKChat widget

Chat widget

Add an AI-powered chat widget. Enable in init, control with flat methods that are safe to call before the widget loads.

Add an AI-powered chat widget to your site. Enable it in init and control it with flat methods like vt.openChat() and vt.sendChatMessage(). All methods are safe to call immediately after init() — the SDK queues calls internally until the feature is ready.

#Enable

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  chat: { enabled: true },
})
typescript

The widget script lazy-loads the first time it's needed when you use the default array.js snippet. Until then, the bubble is server-styled by the SDK config; the heavyweight UI only ships when the user opens the panel.

Tip

Tip: If you call vt.sendChatMessage() or vt.openChat() immediately after page load, use array.chat.js instead of the default snippet — it bundles core + chat in one file with no extra chat.js fetch.

#Open, close & toggle

// Open the chat widget (lazy-loads the full widget if needed)
vt.openChat()

// Close the chat widget
vt.closeChat()

// Toggle between open and closed
vt.toggleChat()
typescript

#Show & hide bubble

// Show the chat bubble (does NOT open the widget)
vt.showChat()

// Hide the chat bubble entirely
vt.hideChat()
typescript

#Send messages

One method handles sending, routing, and opening the panel (open defaults to true):

// Active conversation — opens the panel, then sends (plain text).
// If no conversation is active (first visit, empty inbox, or the user
// is on the channel list), a new conversation is created automatically.
vt.sendChatMessage('Follow-up question')

// Markdown (same styling as AI replies in the widget and inbox)
vt.sendChatMessage({ markdown: '**Billing** question about invoice `#1042`' })

// Or plain string + format option
vt.sendChatMessage('**Billing** question', { format: 'markdown' })

// Limited HTML (sanitized on display)
vt.sendChatMessage({
  html: '<p>See our <a href="/pricing">pricing</a> page.</p>',
})

// Send without opening the panel
vt.sendChatMessage('Follow-up question', { open: false })

// Force a brand-new conversation even when one is already active
vt.sendChatMessage('I need help with billing', { channel: 'new' })

// Message in a specific existing channel
vt.sendChatMessage('Ping', { channel: 'CHANNEL_UUID' })

// Form / CTA injection — AI knows the visitor did not type this manually
vt.sendChatMessage('Interested in the enterprise plan', {
  source: 'landing_form',
  channel: 'new',
})
typescript
OptionDefaultDescription
channel—'new' always creates a channel; a UUID selects one. Omit to use the active conversation, or create one automatically if there is none.
opentrueOpen the widget panel before sending. Pass open: false to send in the background.
formattextWhen the first argument is a string: text, markdown, or html.
source—Slug key (e.g. landing_form) marking the message as automatically sent from your site — form submit, CTA, etc. Stored as message_source on the message so the AI can respond differently than for typed chat. Letters, numbers, _, -, max 64 characters.

Note: Pair source with instructions in your project AI system prompt (Chat settings in the dashboard). For example: When the visitor's message has source landing_form, thank them for the demo request and ask one qualifying question.

The first argument can be a string (plain by default), { markdown: '...' }, { html: '...' }, or { text: '...' } for explicit plain text.

#Delivery reliability

vt.sendChatMessage() returns a Promise<void> that resolves when your message row is saved on the server (not when the AI finishes replying). On poor networks the widget keeps the bubble visible, retries automatically, and queues unsent messages in sessionStorage so a refresh in the same tab can finish delivery.

Important: If you send from a form and immediately redirect, await the promise before navigating so the browser can finish the request:

await vt.sendChatMessage('Enterprise plan inquiry', {
  source: 'landing_form',
  channel: 'new',
})
window.location.href = '/thank-you'
typescript

If you redirect without awaiting, the outbox still retries on the next page load in the same tab, but awaiting is the reliable pattern for form submissions.

Optional callback when the user row is persisted (before the AI stream ends):

vt.init('YOUR_PROJECT_TOKEN', {
  chat: {
    enabled: true,
    onMessageDelivered: ({ channelId, messageId }) => {
      console.log('Saved', channelId, messageId)
    },
  },
})
typescript

#API reference

#Widget chrome

MethodDescription
vt.openChat()Open the widget panel. Lazy-loads the full chat script on first call.
vt.closeChat()Close the widget panel.
vt.toggleChat()Toggle the widget. If not yet loaded, behaves like openChat().
vt.showChat()Show the chat bubble without opening the widget.
vt.hideChat()Hide the chat bubble entirely.

#Messaging

MethodDescription
vt.sendChatMessage(content, options?)Send a message; returns a Promise that resolves when the user message is persisted. Queued if the widget is not loaded yet.

#Bubble position and offset

Chat options (position, bubble.offset, theme, and the rest) can be changed at runtime with the same vt.updateConfig() used for all SDK settings — pass a partial patch; top-level keys shallow-merge, and nested chat deep-merges so you do not wipe other chat fields from init. See Install — Update config at runtime.

The bubble sits in the bottom-right corner by default (position: 'bottom-right'). Switch to the left with position: 'bottom-left'.

To clear host floating buttons, cookie banners, or mobile tab bars, set a pixel inset with bubble.offset instead of custom CSS — offsets are applied inside the widget and survive UI updates.

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  chat: {
    enabled: true,
    position: 'bottom-right',
    bubble: {
      offset: { bottom: 88, right: 24 },
    },
  },
})
typescript
FieldDefaultDescription
bottom20Distance from the bottom of the viewport (px).
right20Used when position is bottom-right (px).
left20Used when position is bottom-left (px).

Omit a field to keep the default for that edge. Pass bubble: { offset: null } inside vt.updateConfig({ chat: { bubble: { offset: null } } }) to reset all edges to 20px.

Important

Important: When bubble.draggable is true, the visitor’s dragged position is stored in localStorage and overrides bubble.offset and any later chat patches from vt.updateConfig(). The SDK does not re-apply inset while draggable is enabled. Use either per-route offsets or a draggable bubble — not both.

#Change bubble position on a specific route

Typical case: A fixed cart, help, or cookie-control button sits in the bottom-right on /checkout or /pricing. The chat bubble uses the same corner by default and overlaps it. Raise the bubble on those routes only — without overriding vTilt styles in your own CSS (those overrides often break when the widget UI updates).

When the route changes, patch only the chat slice of SDK config:

vt.updateConfig({
  chat: { bubble: { offset: { bottom: 88, right: 24 } } },
})
typescript

The chat object deep-merges with options from init, so you only send what changed. Other top-level SDK keys (autocapture, capture_pageview, …) are untouched. Both the lightweight pre-load bubble and the full widget apply the new inset immediately (unless bubble.draggable is enabled — see above).

  1. Set your default offset in init (optional — defaults are 20px on each edge).
  2. On each navigation, call vt.updateConfig({ chat: { bubble: { offset } } }) with route-specific values.
  3. When leaving a conflicting route, pass the default offset again (or offset: null to reset to 20px).
// Shared helper — import from a small module in your app
const DEFAULT_BUBBLE_OFFSET = { bottom: 20, right: 20, left: 20 }

/** Routes where a host FAB occupies the bottom-right corner. */
const ROUTES_WITH_FAB = ['/checkout', '/pricing']

const RAISED_BUBBLE_OFFSET = { bottom: 88, right: 24, left: 20 }

export function syncChatBubbleForPath(pathname: string): void {
  const needsLift = ROUTES_WITH_FAB.some(route => pathname.startsWith(route))
  vt.updateConfig({
    chat: {
      bubble: {
        offset: needsLift ? RAISED_BUBBLE_OFFSET : DEFAULT_BUBBLE_OFFSET,
      },
    },
  })
}
typescript

Wire the helper to your router so it runs once per navigation (in useEffect with [pathname], or router.afterEach in Vue/Angular). Do not call vt.updateConfig() on every render.

Add a client component next to your existing VTiltProvider (same layout that calls vt.init):

// app/providers/vtilt-chat-bubble.tsx
'use client'

import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
import { syncChatBubbleForPath } from '@/lib/vtilt-chat-bubble'

export function VTiltChatBubbleLayout() {
  const pathname = usePathname()

  useEffect(() => {
    syncChatBubbleForPath(pathname)
  }, [pathname])

  return null
}
tsx
// app/layout.tsx
import { VTiltProvider } from './providers/vtilt-provider'
import { VTiltChatBubbleLayout } from './providers/vtilt-chat-bubble'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <VTiltProvider />
        <VTiltChatBubbleLayout />
        {children}
      </body>
    </html>
  )
}
tsx
// lib/vtilt-chat-bubble.ts — same helper as above (DEFAULT_BUBBLE_OFFSET, syncChatBubbleForPath)
typescript

Tip: If the overlap is severe or the launcher should live inside your nav, hide the default bubble (bubble: { visible: false }) and open chat from your own button with vt.openChat() — see Hidden bubble with programmatic show.

Note

Note: On viewports ≤480px the open panel goes fullscreen; bubble offset mainly affects the closed launcher position. Test on real devices if mobile tab bars also occupy the bottom edge.

#Hidden bubble with programmatic show

Start with the bubble hidden and show it when ready — for example after onboarding or on a specific page.

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  chat: {
    enabled: true,
    bubble: { visible: false },
  },
})

// Show the bubble programmatically (e.g. after onboarding)
vt.showChat()
typescript

#Event callbacks

React to widget lifecycle events with callbacks in the chat config.

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  chat: {
    enabled: true,
    onWidgetOpen: () => {
      console.log('Chat opened')
    },
    onWidgetClose: ({ timeOpenSeconds, messagesSent }) => {
      console.log(
        `Chat closed after ${timeOpenSeconds}s, ${messagesSent} messages`,
      )
    },
    onConversationStart: ({ channelId, aiMode }) => {
      console.log(`New conversation ${channelId}, AI: ${aiMode}`)
    },
  },
})
typescript
CallbackPayloadDescription
onWidgetOpen—Fired when the widget panel opens.
onWidgetClose{ timeOpenSeconds, messagesSent }Fired when the widget panel closes, with session stats.
onConversationStart{ channelId, aiMode }Fired when a new conversation begins.
onMessageSent{ channelId, messageId }Fired when the user sends a message.
onMessageReceived{ channelId, messageId, senderType }Fired when a message is received from AI or an agent.
PreviousSession recordingBrowser SDKNextFeature readinessBrowser SDK

On this page

  • Enable
  • Open, close & toggle
  • Show & hide bubble
  • Send messages
  • Delivery reliability
  • API reference
  • Widget chrome
  • Messaging
  • Bubble position and offset
  • Change bubble position on a specific route
  • Hidden bubble with programmatic show
  • Event callbacks