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:
| Attribute | Values | Notes |
|---|---|---|
data-api-key | Agent-scoped key with chat permission | Required for auto-init; omit for programmatic setup |
data-mode | suite | Omit for launcher mode |
data-position | bottom-right, bottom-left, side-panel | Mobile uses a bottom sheet layout |
data-base-url | Kravos web app origin | Optional; 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:
| Direction | Message | Payload |
|---|---|---|
| Iframe → host | single-chat:presentation | { teaser, unreadCount, activeCall, surfaceColor, surfaceInk } |
| Host → iframe | single-chat:presentation-request | none; 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.
| Area | Runtime behavior |
|---|---|
| Agent | Selected by the agent-scoped API key |
| Surface | launcher by default, suite when mode=suite |
| Placement | Loader supports bottom-right, bottom-left, and side-panel; mobile is bottom |
| Theme | Primary color, header text, bubbles, input colors, font, border radius |
| Branding | Title, subtitle, logo URL, and powered-by badge |
| Behavior | Greeting and input placeholder |
| Launcher | Launcher teaser opt-in, unread badge, and first-surface entry preference |
| Pre-chat | Optional required name, email, and phone fields before chat starts |
| Voice | Launcher 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.
| State | What the visitor sees |
|---|---|
| Teaser | Optional welcome card beside the launcher when Launcher teaser is on |
| Unread badge | Unread conversation count on the launcher button, shown above zero |
| Active call | Phone launcher that returns to the running voice call |
| Open | Close 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.
| Input | Purpose | Does not provide |
|---|---|---|
user / setUser({ name, email, phone }) | Unsigned profile and pre-chat information | Verified identity or automatic model context |
identify({ identityToken }) | Signed identity from your configured host-server issuer | Per-turn application context |
context / setContext() | Deliberate model-visible facts | Authorization 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.
| Field | Model | Model provider | Configured Kravos tools | Durable runtime-input storage |
|---|---|---|---|---|
context | Yes | Yes | Not automatically | No |
toolContext | No | No | Explicit selected bindings | No |
toolCredentials | Never | Never | Explicit selected auth slots | Never |
Navigate without recreating the widget
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:
| Route | Purpose |
|---|---|
POST /api/sdk/init | Creates/resumes session, user, config, preferences, history |
POST /api/chat/stream | Sends launcher text/attachments and streams the response |
POST /api/v1/chat/stream | Sends suite text/attachments and streams the response |
GET /api/sdk/conversation | Refreshes the active launcher conversation |
PATCH /api/sdk/conversation | Closes a launcher conversation |
POST /api/sdk/new-chat | Starts a fresh text conversation |
GET/POST /api/sdk/conversations | Lists or creates suite conversations |
GET/PATCH /api/sdk/conversations/:id | Reads, renames, or archives a suite conversation |
GET /api/sdk/conversations/search | Searches suite conversation titles and messages |
GET/PATCH /api/sdk/preferences | Reads or updates locale, sidebar state, and last conversation |
PATCH /api/sdk/user | Updates unsigned user fields for the current session |
POST /api/sdk/feedback | Stores conversation-level rating and optional comment |
POST /api/sdk/voice/live/start | Starts or deliberately resumes a native WebRTC call |
POST /api/sdk/voice/live/end | Fences 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
| Symptom | Check |
|---|---|
API key is required | Supply data-api-key for auto-init or apiKey to SingleChat.init(); a custom iframe host needs a valid exact-origin bootstrap. |
401 Invalid API key | The key is missing, revoked, expired, wrong prefix, or lacks chat permission. |
403 agent-scoped API key | SDK routes require an agent-scoped key. Organization-scoped keys are for other API surfaces. |
403 Origin is not allowed | Allowed origins are exact strings. Use origins like https://app.example.com, not paths or trailing slashes. |
| Widget works in sandbox only | Dashboard session fallback can hide key/domain issues. Test with the production script on a real staging URL. |
| Cross-origin API calls fail | The hosted widget calls /api/sdk/* from inside the iframe. Contact Kravos.ai support before making direct browser calls from your app. |
| Voice button missing | Confirm 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
Related Docs
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.


