Widget and SDK Setup

Use the widget when you want Kravos.ai to own the customer chat surface: launcher button, iframe, session setup,

Widget and SDK Setup

Use the widget when you want Kravos.ai to own the customer chat surface: launcher button, iframe, session setup, pre-chat collection, voice handoff, end-chat feedback, and conversation history. Use the SDK and dashboard-domain SDK routes when you need to embed that same surface inside your product or build a thin custom wrapper around it.

Dashboard-domain, not public API v1

Widget routes live on your Kravos web app domain, for example https://app.kravos.ai/widget, /sdk/v1/loader.js, /api/sdk/*, and /api/chat/stream. The server-to-server public API lives at https://api.kravos.ai/v1. Do not move widget calls to the public API host unless the endpoint is explicitly listed in API Reference.

Quickstart

Create an agent-scoped chat key

Open the agent's API keys page and create a key with the chat permission. The widget chooses its agent from this key. There is no public agentId query parameter.

Configure the widget in the dashboard

Use Widget Settings to set colors, greeting text, pre-chat fields, voice, and embed surface.

Install on a staging page

Start with the script snippet from the Embed tab. Test the real page, not only the dashboard preview, because placement and domain allowlists are page-dependent.

Check the first conversations

After launch, review Conversations, Feedbacks, and Analytics for early traffic and answer quality.

Install Options

Script loader

The dashboard embed tab generates this option. It injects the iframe, adds a floating launcher button, validates messages from the iframe origin, and starts minimized.

<script src="https://app.kravos.ai/sdk/v1/loader.js" data-api-key="spk_..." data-position="bottom-right" async></script>

For the in-app suite surface, add data-mode="suite". The dashboard uses side-panel for suite installs.

<script
  src="https://app.kravos.ai/sdk/v1/loader.js"
  data-api-key="spk_..."
  data-mode="suite"
  data-position="side-panel"
  async
></script>

Supported data attributes:

AttributeValuesNotes
data-api-keyAgent-scoped key with chat permissionRequired for auto-init; omit for programmatic setup
data-modesuiteOmit for launcher mode
data-positionbottom-right, bottom-left, side-panelMobile uses a bottom sheet layout
data-base-urlKravos web app originOptional; use only when instructed by Kravos.ai support

Programmatic loader: help with the current order

Use the hosted loader when your application supplies user context or owns navigation and logout handlers. Load it once after the document body exists, then retain the instance returned by SingleChat.init():

<!-- Ordered scripts: do not add async or data-api-key to this programmatic setup. -->
<script src="https://app.kravos.ai/sdk/v1/loader.js"></script>
<script>
  const widget = SingleChat.init({
    apiKey: 'spk_...',
    baseUrl: 'https://app.kravos.ai',
    mode: 'launcher',
    position: 'bottom-right',
    user: { name: 'Jane', email: 'jane@example.com' },
    context: {
      user: { displayName: 'Jane', locale: 'en' },
      page: { name: 'Order details', selectedOrderId: 'order_123' },
    },
  })
</script>

These are synthetic values; derive production values from your current application state. Initialize after that state is available or update the retained instance when it changes. In a framework, load the script on the client, wait for it to finish loading, and keep one instance in the host lifecycle owner—not one per render. Adding data-api-key would auto-initialize before your manual call; a second init() does not retrieve the existing working instance. The loader handles exact-window and exact-origin bootstrap automatically.

From your own event handler, call widget.open() to open the chat, or widget.open({ entry: 'chat' | 'voice' }) to choose the first surface for that open. entry: 'voice' shows the disclosed Talk/Chat choice and never starts audio; entry: 'chat' goes straight to the composer. The host entry overrides the dashboard entry preference. Use mode: 'suite' and position: 'side-panel' for a programmatic suite install. See runtime updates and logout below.

Direct iframe

Use a direct iframe when your app already owns the launcher button, page layout, and bootstrap message. The supported SDKs handle this handshake automatically; a hand-built iframe must enforce the same exact-window and exact-origin checks.

<iframe
  id="kravos-widget"
  src="https://app.kravos.ai/widget?mode=launcher"
  title="Chat Widget"
  allow="microphone; speaker-selection"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>

The iframe first sends single-chat:ready. Verify that the event came from this iframe and from

https://app.kravos.ai, then send one single-chat:bootstrap message back to that exact origin with the publishable key, your exact parent origin, and optional runtime data. Never use * as the bootstrap target. Use mode=suite for the suite surface; placement remains host-page CSS.

Direct legacy embeds with ?key=spk_... remain readable during the compatibility window, but SDK-generated URLs are keyless and should use /widget?mode=launcher. Do not build a new integration around the legacy URL.

The iframe can also publish a presentation snapshot for a host-drawn launcher. Both messages are additive; hosts and iframes built before them simply ignore the unknown type:

DirectionMessagePayload
Iframe → hostsingle-chat:presentation{ teaser, unreadCount, activeCall, surfaceColor, surfaceInk }
Host → iframesingle-chat:presentation-requestnone; asks the iframe to republish

teaser is { title, subtitle, message, voiceAvailable } or null; unreadCount is an integer capped at 999; surfaceColor and surfaceInk are six-digit hex colors used for host-drawn sheet chrome. Every field defaults to its neutral value when absent, and a malformed snapshot is ignored as a whole. Send a presentation request after single-chat:theme-ready, after single-chat:identify, and after single-chat:reset-session so the host snapshot resets with the session.

Publishable keys are browser-visible

Keyless iframe URLs prevent accidental URL, referrer, and metadata propagation. They do not make a publishable key secret: it remains browser-visible in host memory, bootstrap messages, and request headers. Scope the key to one agent, grant only the permissions you need, and configure exact allowed origins.

Package factory

The repo includes @kravos.ai/sdk with createChatWidget, useWidget, useChat, and

RealtimeConnection. The package is not currently distributed for customer use; use the script or iframe integration instead.

Realtime hooks are low level

useChat and RealtimeConnection manage socket messages and typing state. The current useChat().sendMessage() only adds an optimistic local message and stops typing; it does not call the hosted chat streaming endpoint for you.

Configuration

Most widget behavior is dashboard-managed and stored on the selected agent. The runtime reads that config during

POST /api/sdk/init.

AreaRuntime behavior
AgentSelected by the agent-scoped API key
Surfacelauncher by default, suite when mode=suite
PlacementLoader supports bottom-right, bottom-left, and side-panel; mobile is bottom
ThemePrimary color, header text, bubbles, input colors, font, border radius
BrandingTitle, subtitle, logo URL, and powered-by badge
BehaviorGreeting and input placeholder
LauncherLauncher teaser opt-in, unread badge, and first-surface entry preference
Pre-chatOptional required name, email, and phone fields before chat starts
VoiceLauncher voice option when widget or agent voice config is enabled

Some saved schema fields are not currently applied by the hosted runtime, including persistSession,

showTimestamps, avatarUrl, and iconColor. The dashboard also exposes auto-open settings, but the current script loader starts minimized. Use package or loader programmatic control if you need to open the widget from your own event.

Launcher Presentation

The launcher is host-page UI drawn by the loader. It reflects what the embedded widget publishes; the dashboard owns the content.

StateWhat the visitor sees
TeaserOptional welcome card beside the launcher when Launcher teaser is on
Unread badgeUnread conversation count on the launcher button, shown above zero
Active callPhone launcher that returns to the running voice call
OpenClose icon; the launcher announces its expanded state to assistive tech

The teaser shows the configured chat title, subtitle, and welcome message, and offers Talk only when voice is available. It never opens the widget on its own, and a visitor who dismisses it stays dismissed for that browser session. The unread badge uses authoritative conversation read state; a reloaded iframe publishes a fresh snapshot once it is ready.

On mobile viewports the launcher opens as a bottom sheet instead of a floating window: drag the handle down to dismiss, or close it from the widget. Closing restores page scroll and returns focus to the launcher. The sheet keeps clear of the safe area and the on-screen keyboard. Desktop hosts keep their configured bottom-right, bottom-left, or side-panel placement.

See Direct iframe for the presentation messages a custom host must exchange to drive its own launcher.

Identity and External IDs

By default, the widget creates or resumes an anonymous embedded user using a browser session id stored under

single_chat_session_id. Passing unsigned user data can fill name, email, and phone, but it is not proof of identity.

For logged-in products, use signed embedded identity. The iframe accepts identityToken through

identify() or a single-chat:identify postMessage. The server verifies a short-lived HMAC token with these claims:

{
  "tenantId": "tenant_...",
  "agentId": "agent_...",
  "externalUserId": "user_123",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "avatarUrl": "https://example.com/jane.png",
  "claims": { "plan": "pro" }
}

Issue the token on your server, never in browser code. This app verifies signed identity, but it does not expose a public endpoint that creates identity tokens for you. If Kravos.ai has not configured an issuer for your account, use anonymous sessions or unsigned profile fields instead.

InputPurposeDoes not provide
user / setUser({ name, email, phone })Unsigned profile and pre-chat informationVerified identity or automatic model context
identify({ identityToken })Signed identity from your configured host-server issuerPer-turn application context
context / setContext()Deliberate model-visible factsAuthorization or secret storage

If the model should know a display name, include a suitable non-secret value in context, as in the loader example. An account ID in context grants no downstream access. Once your server has issued a token under the requirements above, pass it as widget.identify({ identityToken }); never put its signing secret in browser code.

Runtime Context and Tool Credentials

The widget accepts three separate runtime data channels for text turns. Runtime input is not stored as conversation metadata, but the model may repeat context in a reply that is saved normally. Context replacement is not erasure of prior conversation content.

FieldModelModel providerConfigured Kravos toolsDurable runtime-input storage
contextYesYesNot automaticallyNo
toolContextNoNoExplicit selected bindingsNo
toolCredentialsNeverNeverExplicit selected auth slotsNever

Call this from the host application's navigation or order-selection handler, using the instance from programmatic setup:

widget.setContext({
  user: { displayName: 'Jane', locale: 'en' },
  page: { name: 'Order details', selectedOrderId: 'order_789' },
})

Build the complete desired snapshot from host state so retained fields are not accidentally removed. context and toolContext are JSON objects whose values may recursively be strings, finite numbers, booleans, null, arrays, or objects. toolCredentials maps slot names to strings.

The loader and package SDK expose the same runtime fields and methods. Each setter replaces its respective in-memory object; setters do not merge nested values. Pass {} to clear that field. Setters are fire-and-forget postMessage updates and do not wait for an iframe acknowledgement. A text turn captures an immutable snapshot when it starts. Credential rotation affects later turns, not a request already sent to an external service.

Optional private-data lookup through configured tools

// Values supplied by the host's authorized session, not literals embedded in source code.
widget.setToolContext({ accountId: currentAccountId })
widget.setToolCredentials({ orders: shortLivedUserToken })

Configure stored bindings and an orders credential slot before using this example. Follow the HTTP tool binding guidance or external MCP server binding guidance; unbound values are ignored by tool execution. Hidden context is still untrusted input; the downstream service must authorize each request. Do not place secrets in context, toolContext, URLs, static headers, custom system prompts, or page telemetry. context is deliberately model-visible in text turns. If a value must remain hidden from the model or provider, put non-secret binding data in toolContext or a short-lived credential in toolCredentials.

Logout, account changes, and teardown

// In the host logout handler, after stopping callbacks that could restore old customer data:
widget.resetSession()

// When permanently removing the integration from the page:
widget.destroy()

Reset clears runtime data and starts the existing anonymous-session lifecycle. Destroy removes the integration; neither method logs out of the host application itself. On account changes, clear or replace account-specific host state and use the reset and signed-identity lifecycle as appropriate. Do not assume identify() clears the previous account's context. Keep new interactions unavailable during the intended identity transition; these methods are not awaitable acknowledgement barriers.

The old browser-mediated voice context and tool relay is retired. Runtime setters do not send values through that path. identify() ends active voice and clears credentials while preserving context and toolContext. resetSession() ends voice and clears all three fields; destroy() ends local capture and clears widget state. An external side effect already accepted downstream cannot be undone, but stale results are not returned to the replaced session.

Legacy top-level context.pageUrl and context.pageTitle are telemetry compatibility fields, not automatic model context. Prefer explicit fields such as page.name and page.selectedOrderId; never copy token-bearing URLs into context.

Conversation Lifecycle

Launcher mode creates or resumes the latest open web text conversation for the session. The user can end the chat, which closes the conversation and shows a 1–5 star feedback form with an optional comment.

Suite mode adds conversation history. It can list, create, open, rename, archive, and search the embedded user's web text conversations. The user's last opened suite conversation is stored in embedded preferences. On wide surfaces the thread and composer stay in a centered column; drag either edge (or use the arrow keys on a focused edge) to change its width, and the widget remembers that width for the browser.

Both surfaces accept images and PDFs for AI turns using the shared attachment pipeline. Launcher mode also accepts an attachment without text. Sent images have inline previews and a lightbox in the open conversation. Those previews are browser-local and are released when that conversation is cleared or the chat is unmounted. Files are supplied to the model for that turn; reloaded history retains file names, types, and sizes, not downloadable copies. Attachment sends require an AI-active conversation. Launcher text messages continue through human takeover, and launcher attachment turns use the same interceptor rules as text turns.

Important routes used by the widget and suite:

RoutePurpose
POST /api/sdk/initCreates/resumes session, user, config, preferences, history
POST /api/chat/streamSends launcher text/attachments and streams the response
POST /api/v1/chat/streamSends suite text/attachments and streams the response
GET /api/sdk/conversationRefreshes the active launcher conversation
PATCH /api/sdk/conversationCloses a launcher conversation
POST /api/sdk/new-chatStarts a fresh text conversation
GET/POST /api/sdk/conversationsLists or creates suite conversations
GET/PATCH /api/sdk/conversations/:idReads, renames, or archives a suite conversation
GET /api/sdk/conversations/searchSearches suite conversation titles and messages
GET/PATCH /api/sdk/preferencesReads or updates locale, sidebar state, and last conversation
PATCH /api/sdk/userUpdates unsigned user fields for the current session
POST /api/sdk/feedbackStores conversation-level rating and optional comment
POST /api/sdk/voice/live/startStarts or deliberately resumes a native WebRTC call
POST /api/sdk/voice/live/endFences a native call or connection by its original identity

Voice in the Widget

Voice appears in launcher mode when widget voice or the agent voice config is enabled. Starting voice requires:

  • enabled local agent voice settings and native admissions
  • a configured native coordinator and server-side OpenAI access
  • enough hosted credits and voice entitlement
  • microphone permission in the browser
  • an iframe or loader with microphone permission

If voice fails to start, the launcher shows an error; text chat remains available. No provider fallback is attempted. Suite init currently reports voice support as disabled.

The voice-first entry preference and open({ entry: 'voice' }) both open the disclosed Talk/Chat choice; neither starts audio by itself. Closing the widget ends an active call, so returning to the page never leaves a hidden microphone session running.

Native voice uses the agent's backend task runtime for configured tools, knowledge and skills. The retired browser relay is not a native credential-forwarding mechanism. Browser-only widget-session credentials are not provisioned to the voice provider. See Voice for lifecycle, recording and compatibility behavior.

Auth, CORS, and Domain Troubleshooting

SymptomCheck
API key is requiredSupply data-api-key for auto-init or apiKey to SingleChat.init(); a custom iframe host needs a valid exact-origin bootstrap.
401 Invalid API keyThe key is missing, revoked, expired, wrong prefix, or lacks chat permission.
403 agent-scoped API keySDK routes require an agent-scoped key. Organization-scoped keys are for other API surfaces.
403 Origin is not allowedAllowed origins are exact strings. Use origins like https://app.example.com, not paths or trailing slashes.
Widget works in sandbox onlyDashboard session fallback can hide key/domain issues. Test with the production script on a real staging URL.
Cross-origin API calls failThe hosted widget calls /api/sdk/* from inside the iframe. Contact Kravos.ai support before making direct browser calls from your app.
Voice button missingConfirm launcher mode, widget/agent voice enabled, and voice config is complete.

If you configure an origin allowlist, include the Kravos web app origin that serves /widget. Include your product origin too only if your page directly calls SDK routes or hosts the loader from that origin.

Before You Go Live

  • create a dedicated agent-scoped chat key for the widget
  • verify allowed origins with the exact staging and production origins
  • test launcher and suite modes separately if you use both
  • check desktop, mobile, and side-panel placement on real pages
  • confirm the launcher teaser, unread badge, and mobile sheet on a phone-sized viewport
  • confirm pre-chat fields are only the fields support teams actually need
  • test signed identity with one returning user and one anonymous user
  • start and end a voice session if voice is enabled
  • submit feedback and confirm it appears in Feedbacks

API Keys

Create agent-scoped keys, set permissions, and manage allowed origins.

Conversations

Review text, suite, and voice conversations after users chat through the widget.

Voice Lab

Test native voice settings, captions and backend tasks before enabling widget voice.

API Reference

Compare dashboard-domain widget routes with the public server-to-server API.

Last updated: September 2026