Web Chat Widget
Embed your ViksaAI agents on any HTTPS website. The widget talks directly to volt-engine-service — no webhook on your site.
Architecture#
The Web Chat Widget is a first-party channel: the visitor's browser calls https://api.viksaai.com/v1/widget over HTTPS. Replies stream back via Server-Sent Events (SSE). Routing uses your widget ID (ww_…).
Public API reference#
| Endpoint | Method | Authentication |
|---|---|---|
| https://api.viksaai.com/v1/widget/{widget_id}/config | GET | Browser Origin (allowlist) |
| https://api.viksaai.com/v1/widget/{widget_id}/session | POST | Browser Origin (allowlist) |
| https://api.viksaai.com/v1/widget/{widget_id}/visitor-token | POST | Authorization: Bearer {widget_secret} (server-to-server only) |
| https://api.viksaai.com/v1/widget/{widget_id}/history | GET | Origin + Authorization: Bearer {session_jwt} |
| https://api.viksaai.com/v1/widget/{widget_id}/messages | POST (SSE) | Origin + session JWT; optional X-Visitor-Token |
All browser endpoints require a valid Origin header matching your allowlist (Referer is not accepted). Error responses use uniform 404 widget_not_found where appropriate to prevent enumeration.
GET /config — Returns appearance (title, colors, logo). No auth beyond origin.
{
"widget_id": "ww_your_widget_id",
"appearance": {
"title": "Chat with us",
"primary_color": "#d4e633",
"position": "bottom-right",
"greeting": "Hi! How can we help?",
"placeholder": "Type a message…",
"logo_url": "https://cdn.example.com/logo.png"
}
}POST /session — Creates a chat session. Call from the browser.
curl -s -X POST "https://api.viksaai.com/v1/widget/ww_your_widget_id/session" \
-H "Origin: https://app.example.com" \
-H "Content-Type: application/json" \
-d '{}'{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"token": "<session-jwt>",
"expires_in": 14400
}POST /visitor-token — Mint a visitor JWT from your backend (recommended). Authenticate with your widget secret (server-to-server only).
curl -s -X POST "https://api.viksaai.com/v1/widget/ww_your_widget_id/visitor-token" \
-H "Authorization: Bearer $VIKSA_WIDGET_SECRET" \
-H "Content-Type: application/json" \
-d '{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "[email protected]",
"name": "Jane Doe",
"origin": "https://app.example.com"
}'{
"visitor_token": "<visitor-jwt>",
"expires_in": 900
}GET /history — Requires session Bearer JWT.
curl -s "https://api.viksaai.com/v1/widget/ww_your_widget_id/history" \
-H "Origin: https://app.example.com" \
-H "Authorization: Bearer <session-jwt>"POST /messages — Sends a message; response is SSE. Include optional visitor JWT for authenticated users.
curl -N -X POST "https://api.viksaai.com/v1/widget/ww_your_widget_id/messages" \
-H "Origin: https://app.example.com" \
-H "Authorization: Bearer <session-jwt>" \
-H "X-Visitor-Token: <visitor-jwt>" \
-H "Content-Type: application/json" \
-d '{"text": "Hello", "client_msg_id": "msg-001"}'data: {"type":"phase","phase":"turn_started"}
data: {"type":"summary_delta","text":"Hello"}
data: {"type":"summary_delta","text":"! How can I help?"}
data: {"type":"message","message":"Hello! How can I help?"}
data: {"type":"done","success":true}JWT claims#
Both token types use HS256. HS256 with key sha256(widget_secret) as hex string. Same algorithm for self-signing or verification. Retrieve your widget secret from Channel Hub → Web Chat Widget → Connection → Reveal secret (project admin only). Store it in an environment variable on your server — never in the browser.
| Claim | Session JWT | Visitor JWT |
|---|---|---|
| typ | widget_session | widget_visitor |
| wid | Widget ID | Widget ID |
| sid | Session UUID | Must match session JWT sid |
| org | HTTPS origin | Must match browser Origin |
| — | User email (access grant lookup) | |
| name | — | Optional display name |
| exp | ~4h default | ~15 min default |
Access control#
Web Chat uses channel grants only — not your Viksa project RBAC. Configure grants under Channel Hub → Web Chat Widget → Access.
| Visitor type | Identity sent | Grant type |
|---|---|---|
| Logged-in user | Visitor JWT email | Email grant (e.g. [email protected] → agent) |
| Anonymous | Session UUID only | Session ID grant or wildcard * |
| Public widget | No visitor JWT + * grant | All agents (if you allow it) |
Optional allowed visitor email domains (Widget tab) restrict which emails may appear in visitor JWTs (e.g. example.com only).
Authenticated embed (recommended flow)#
- 1
User logs in on your site
Your normal auth (OAuth, session cookie, etc.) stays on your backend. Viksa never sees it.
- 2
Browser creates a widget session
POST /sessionfrom the browser (Origin header sent automatically). Savesession_idand session JWT. - 3
Your backend mints a visitor JWT
Call
POST /visitor-tokenwith the widget secret, or sign the JWT yourself (see below). Pass the logged-in user's email and the samesession_id. - 4
Browser loads the widget with both tokens
Set
window.ViksaChatConfig = { visitorToken }and pre-seed session storage, then loadviksa-chat.js.
<script>
(async function () {
const widgetId = "ww_your_widget_id";
const apiBase = "https://api.viksaai.com/v1/widget";
const origin = window.location.origin;
// 1) Create widget session (browser → Viksa)
const sess = await fetch(apiBase + "/" + widgetId + "/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
}).then((r) => r.json());
// 2) Ask YOUR backend for a visitor JWT (uses your app login + widget secret)
const { visitorToken } = await fetch("/api/viksa/visitor-token", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sess.session_id, origin }),
}).then((r) => r.json());
// 3) Pre-seed session + config before widget script runs
sessionStorage.setItem(
"viksa_widget_" + widgetId + "_session",
JSON.stringify({ session_id: sess.session_id, token: sess.token }),
);
window.ViksaChatConfig = { visitorToken };
// 4) Load widget
const s = document.createElement("script");
s.src = "https://app.viksaai.com/widget/v1/viksa-chat.js";
s.integrity = "sha384-Zp6RCQUzDfvAlbVwz41y1IvQrigyeCDOaHgUFVMcb3DimIaSSyi2gPjTQGOIJxDC";
s.crossOrigin = "anonymous";
s.async = true;
s.setAttribute("data-widget-id", widgetId);
s.setAttribute("data-api-base", apiBase);
document.body.appendChild(s);
})();
</script>Your backend: mint visitor token#
Expose an endpoint (e.g. POST /api/viksa/visitor-token) that requires your app's user session, then calls Viksa or signs the JWT locally.
Option A — Viksa mint API (simplest)
import os
import httpx
VIKSA_WIDGET_SECRET = os.environ["VIKSA_WIDGET_SECRET"]
WIDGET_ID = os.environ["VIKSA_WIDGET_ID"]
API_BASE = "https://api.viksaai.com/v1/widget"
def mint_visitor_token_for_user(*, session_id: str, origin: str, email: str, name: str | None):
"""Call after your auth middleware validates the logged-in user."""
r = httpx.post(
f"{API_BASE}/{WIDGET_ID}/visitor-token",
headers={"Authorization": f"Bearer {VIKSA_WIDGET_SECRET}"},
json={
"session_id": session_id,
"origin": origin,
"email": email,
"name": name,
},
timeout=10.0,
)
r.raise_for_status()
return r.json()["visitor_token"]Option B — Self-sign JWT (same algorithm Viksa uses)
import hashlib, secrets, time
import jwt
def mint_visitor_token(*, widget_id, widget_secret, session_id, origin, email, name=None):
key = hashlib.sha256(widget_secret.encode()).hexdigest()
now = int(time.time())
payload = {
"typ": "widget_visitor",
"wid": widget_id,
"sid": session_id,
"org": origin.rstrip("/").lower(),
"email": email.strip().lower(),
"iat": now,
"nbf": now,
"exp": now + 900,
"jti": secrets.token_hex(12),
}
if name:
payload["name"] = name[:120]
return jwt.encode(payload, key, algorithm="HS256")// Example: Express route proxying to Viksa mint API
app.post("/api/viksa/visitor-token", requireUserSession, async (req, res) => {
const { session_id, origin } = req.body;
const user = req.user; // from YOUR auth
const viksa = await fetch(`https://api.viksaai.com/v1/widget/${process.env.VIKSA_WIDGET_ID}/visitor-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIKSA_WIDGET_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
session_id,
origin,
email: user.email,
name: user.displayName,
}),
});
if (!viksa.ok) return res.status(viksa.status).end();
const { visitor_token } = await viksa.json();
res.json({ visitorToken: visitor_token });
});Basic embed (anonymous)#
<!-- Anonymous visitors — grant by session UUID or * in Access tab -->
<script
src="https://app.viksaai.com/widget/v1/viksa-chat.js"
integrity="sha384-Zp6RCQUzDfvAlbVwz41y1IvQrigyeCDOaHgUFVMcb3DimIaSSyi2gPjTQGOIJxDC"
crossorigin="anonymous"
data-widget-id="ww_your_widget_id"
data-api-base="https://api.viksaai.com/v1/widget"
async
></script>For logged-in users, do not use a static placeholder JWT. Follow the authenticated flow above.
Setup in Channel Hub#
- 1
Connect the widget
Volt → Channels → Web Chat Widget. Add HTTPS origins, appearance, optional logo and email domains.
- 2
Copy credentials
Connection tab: copy Widget ID, click Reveal secret for server-side minting. Store
VIKSA_WIDGET_SECRETin your backend env. - 3
Configure access grants
Access tab: add email grants for logged-in users or
*for public widgets. - 4
Embed and test
Use the Embed tab snippet or authenticated flow above. Test with the in-dashboard Test tab (impersonate email/session).
Security controls#
- HTTPS origins only — exact match; browser Origin header required
- Widget secret never in browser; visitor mint API is server-to-server only
- Session and visitor JWTs bound to origin + session
- Web Chat access is grant-only (no project RBAC merge)
- Rate limits on session, config, history, messages, and visitor mint
- One active turn per session; idempotency after access check
- Optional allowed visitor email domains
- Subresource Integrity on embed script; Shadow DOM UI
See also: Channel Hub security.