# Introduction
Source: https://docs.scenaro.io/introduction/overview/
Scenaro combines a scenario authoring cockpit, a Python voice agent, a public API, and the `@scenaro/sdk` npm package — everything you need to ship a production voice experience on the web.
## Platform map
```mermaid
flowchart LR
Cockpit[Cockpit] --> API[Platform API]
API --> Agent[Voice Agent]
SDK["@scenaro/sdk"] --> Transport["Transport session"]
Agent --> Transport
App[Your Web App] --> SDK
```
| Component | Role |
|-----------|------|
| **Cockpit** | Author scenarios, configure features, publish to production |
| **Platform API** | Identity, sessions, scenario metadata, collections |
| **Voice Agent** | Python agent joining the Transport session — STT, LLM, TTS, tool calls |
| **`@scenaro/sdk`** | Protocol, API client, React providers and hooks |
| **Transport** | Realtime engine carrying audio + data channel — [LiveKit](https://livekit.io) today, see [Transport](/introduction/transport) |
## Three primitives
Create and publish scenarios in Cockpit. The API issues JWT identity tokens and Transport session credentials.
The Python agent joins the Transport session, runs the conversation, and calls tools — some fulfilled on the frontend via RPC.
Headless TypeScript SDK: protocol types, HTTP client, React bindings. Business features stay in your app.
## Why @scenaro/sdk
Before unification, each experience vendored its own copy of `scenaro-sdk` with diverging auth and storage behavior. `@scenaro/sdk` is now the **single source of truth** for:
1. **Protocol** — Transport data-channel topics, message formats, RPC contracts
2. **API client** — identify, session, token storage
3. **React bindings** — providers, hooks, feature runtime
Business features (product search, cart, comparison) remain **outside the package**. Each app injects its own feature registry.
`@scenaro/sdk` talks to an internal `Transport` interface rather than any realtime engine
directly — LiveKit is the current implementation. See [Transport](/introduction/transport) for
what that means for your code.
## Package structure
```
@scenaro/sdk
├── protocol # Agent ↔ frontend contract (zero dependencies)
├── client # ScenaroClient, JWT, session storage
├── core # ScenaroSession — headless, framework-agnostic
└── react # ScenaroProvider, useScenaroSession, FeatureProvider
```
| Import | Contents | Dependencies |
|--------|----------|--------------|
| `@scenaro/sdk` | `ScenaroSession`, client, protocol (framework-agnostic) | livekit-client (peer) |
| `@scenaro/sdk/protocol` | Topics, message types, RPC | none |
| `@scenaro/sdk/client` | `createScenaroClient`, auth, storage | none (native fetch) |
| `@scenaro/sdk/react` | `ScenaroProvider`, `useScenaroSession`, `FeatureProvider` | react, livekit-client, @livekit/components-react (peers) |
`livekit-client` and `@livekit/components-react` are **peer dependencies** because LiveKit is
the current [Transport](/introduction/transport) implementation. The SDK does not bundle
LiveKit — it builds on top of it. Backend scripts can use `client` + `protocol` without React
or a Transport implementation at all.
## Install
For the embed path — one provider, one hook — you only need the SDK itself:
```bash npm
npm install @scenaro/sdk
```
```bash pnpm
pnpm add @scenaro/sdk
```
```bash yarn
yarn add @scenaro/sdk
```
Current version: **`@scenaro/sdk@0.3.0`**
Reaching for the current engine's own components directly — custom visualizers, avatars,
multi-participant UI? Install the LiveKit peer dependencies as described in
[Advanced: Transport extensions](/guides/advanced-transport-integration). The quickstart below
does not need them.
## Next steps
Full developer journey — install to production
Embed voice on your site in 15 minutes
Connect docs to Cursor, Claude, and ChatGPT
Understand the identify → session flow
Data-channel topics and message contracts
---
# Implement an experience
Source: https://docs.scenaro.io/introduction/implement-an-experience/
This page is the **developer journey map** for building a voice experience with `@scenaro/sdk`.
It follows the same structure as Retell, Vapi, and ElevenLabs docs: install → connect → listen →
handle tools → ship.
Already have a working quickstart? Use this page as a checklist and jump to the sections you need.
## What you are building
```mermaid
flowchart LR
App[Your React app] --> SDK["@scenaro/sdk/react"]
SDK --> API[Platform API]
SDK --> Transport["Transport session"]
Agent[Voice agent] --> Transport
Cockpit[Cockpit scenario] --> Agent
```
| Layer | Your responsibility | Provided by Scenaro |
|-------|---------------------|---------------------|
| **Scenario** | Choose publication UUID | Cockpit authoring + agent |
| **Session** | Call `start()` / `end()` | Identify, JWT, Transport credentials |
| **Transport** | Usually nothing | SDK connects the session + data channel — see [Transport](/introduction/transport) |
| **Tools** | Implement frontend handlers | Agent calls tools via RPC |
| **UI features** | Registry + components | `FeatureProvider` or local runtime |
| **Brand** | CSS, layout, copy | — |
## Prerequisites
- A **published scenario UUID** from Cockpit
- Node.js 18+ and React 18+ (for the React path)
- Microphone access in the browser (for voice mode)
## Step 1 — Install
```bash npm
npm install @scenaro/sdk
```
```bash pnpm
pnpm add @scenaro/sdk
```
Current version: **`@scenaro/sdk@0.3.0`**
This is all you need for the embed path below. Only install `livekit-client` and
`@livekit/components-react` if you reach [Step 7](#step-7-advanced-transport-extensions-optional).
## Step 2 — Choose your integration path
`ScenaroProvider` + `useScenaroSession`. Used by all Scenaro experience apps and platform-spa.
Start here unless you have a strong reason not to.
`ScenaroSession` from `@scenaro/sdk`. Same lifecycle and events, no React bindings.
See [ScenaroSession reference](/sdk-reference/session).
### Provider stack (React)
```tsx
import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react';
export function ScenaroLiveProviders({ children, registry }) {
return (
{children}
);
}
```
`FeatureProvider` is optional — skip it if you only need session tools (see [Tools and features](/build/tools-and-features)).
## Step 3 — Start a session
The SDK handles the full connect flow: identify visitor → start session → connect Transport → config handshake.
```tsx
import { useScenaroSession } from '@scenaro/sdk/react';
const PUBLICATION_UUID = 'your-publication-uuid';
export function VoiceControls() {
const {
status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended'
agentState, // 'idle' | 'listening' | 'thinking' | 'speaking'
start,
end,
sendText,
setMicrophoneEnabled,
} = useScenaroSession({
scenario: PUBLICATION_UUID,
language: 'en-US', // optional — omit to use scenario default
onError: (e) => console.error(e.code),
});
return (
<>
>
);
}
```
Auth is **two-hop**: `identifyUser` then `startSession`. You never call these directly in a
typical embed — `start()` runs both. See [Authentication](/build/authentication).
## Step 4 — Listen to events
Like Retell's `call_started` / Vapi's `call-start` / ElevenLabs' `onConnect`, Scenaro exposes
session state through the hook and typed callbacks:
| What you need | How |
|---------------|-----|
| Connection state | `status`, `agentState` |
| Transcript / messages | `messages` array + `onMessage` |
| Session config | `config` (curated) or `rawConfig` (wire format) |
| Errors | `error` + `onError` |
| Session ended | `onSessionEnd` |
| Cross-component events | `useScenaroEvents()` event bus |
```tsx
const { messages, config } = useScenaroSession({
scenario: PUBLICATION_UUID,
onMessage: (m) => appendToUI(m),
onSessionEnd: (info) => showSummary(info.reason),
});
```
See [Events and listeners](/build/events-and-listener) for the full event catalog.
## Step 5 — Handle frontend tools
When the voice agent calls a tool configured for frontend execution, the SDK routes the RPC to
your handler and returns the result to the agent.
You have **two integration patterns** — pick one per tool:
| Pattern | Best for | Doc |
|---------|----------|-----|
| **Session tools** | Headless logic, no UI panel | `useScenaroSession({ tools })` |
| **Feature tools** | Tool opens a UI panel (search, cart…) | `FeatureProvider` registry |
```tsx
// Session tool — plain function, SDK handles RPC
useScenaroSession({
scenario: PUBLICATION_UUID,
tools: {
search_products: async (args, context) => {
return searchCatalog(args, { signal: context.signal });
},
},
});
```
Read [Tools and features](/build/tools-and-features) before wiring handlers — the two patterns
use **different handler signatures**.
## Step 6 — Build UI features (optional)
For rich experiences (product search, comparison, shopping list), register feature components:
1. Define handlers + a React component per feature type
2. Register in a `Map`
3. Mount `FeatureProvider` with that registry
See [Feature registry](/build/feature-registry) and the [custom feature recipe](/guides/recipes/custom-feature).
Most Scenaro experience apps keep a **local `FeatureManager`** instead of SDK `FeatureProvider`.
Both patterns work on top of `useScenaroSession`. See [Tools and features](/build/tools-and-features#local-featuremanager-vs-sdk-featureprovider).
## Step 7 — Advanced: Transport extensions (optional)
Most embeds never touch the underlying engine directly. When you need custom audio visualizers,
avatars, or participant UI, use `room` from the hook — the current Transport implementation's
engine handle:
```tsx
const { room } = useScenaroSession({ scenario: PUBLICATION_UUID });
// room: LiveKit Room | null (today) — use @livekit/components-react hooks
```
See [Advanced: Transport extensions](/guides/advanced-transport-integration) and
[Transport](/introduction/transport) for the portable-vs-engine-extension distinction.
## Step 8 — Ship to production
Before going live:
- [ ] Env vars: `NEXT_PUBLIC_API_URL` (or equivalent) points to production API
- [ ] Scenario published in Cockpit production environment
- [ ] HTTPS + microphone permission UX
- [ ] Error handling for `IDENTIFY_FAILED`, `SESSION_START_FAILED`, `MIC_PERMISSION_DENIED`
- [ ] Session cleanup on page unload (`end()`)
Full checklist: [Production checklist](/guides/production-checklist).
## Recommended reading order
[15-minute embed](/introduction/quickstart) — minimal working example
[Key concepts](/introduction/concepts) — scenario, auth, protocol
[Provider stack](/build/react-integration) — props, hooks, Next.js
[Tools and features](/build/tools-and-features) — choose the right pattern
Pick a track: [embed](/guides/recipes/embed-voice-website) · [e-commerce](/guides/recipes/ecommerce-embed) · [multi-language](/guides/recipes/multi-language)
[SDK reference](/sdk-reference/react) · [Protocol](/voice-agent/protocol/topics)
## Compare with other platforms
| Theme | Retell | Vapi | ElevenLabs | Scenaro |
|-------|--------|------|------------|---------|
| Web SDK install | `retell-client-js-sdk` | `@vapi-ai/web` | `@elevenlabs/react` | `@scenaro/sdk/react` |
| Start call | `startCall({ accessToken })` | `start(assistantId)` | `startSession({ agentId })` | `start()` via `useScenaroSession` |
| Auth | Server mints token | Public key + assistant ID | Signed URL / token | Two-hop identify + session (automatic) |
| Events | `call_started`, `update` | `call-start`, `message` | `onConnect`, `onMessage` | `status`, `agentState`, `messages` |
| Client tools | — | `tool-calls` (one-way) | `clientTools` | Session tools + FeatureProvider (two-way RPC) |
| Transport | WebRTC audio | WebRTC | WebRTC / WebSocket | Abstracted (LiveKit today) — see [Transport](/introduction/transport) |
Scenaro's differentiator: **scenario-driven features** authored in Cockpit, with a typed
protocol between agent and frontend carried over Transport.
---
# Quickstart
Source: https://docs.scenaro.io/introduction/quickstart/
Get a live voice session running on your site in about 15 minutes. This quickstart covers the
embed path — a single provider, a single hook, no transport concepts to learn.
Building an immersive, highly customized experience (custom audio visualizers, avatars,
multi-participant UI)? See [Advanced: Transport extensions](/guides/advanced-transport-integration)
once you've got the basics running here.
## Prerequisites
- A published scenario UUID from Cockpit
- Node.js 18+ and a React 18+ app
- Microphone access in the browser
## Step 1 — Install the SDK
```bash npm
npm install @scenaro/sdk
```
```bash pnpm
pnpm add @scenaro/sdk
```
## Step 2 — Wrap your app
```tsx
import { ScenaroProvider } from '@scenaro/sdk/react';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
If your API base URL already includes `/v1` (e.g. `NEXT_PUBLIC_API_URL=https://api.scenaro.io/v1`), pass `apiPathPrefix=""` to `ScenaroProvider` to avoid double-prefixing paths.
## Step 3 — Start a session
```tsx
import { useScenaroSession } from '@scenaro/sdk/react';
const SCENARIO_UUID = 'your-publication-uuid';
export function VoiceButton() {
const { status, start, end } = useScenaroSession({ scenario: SCENARIO_UUID });
return status === 'connected'
?
: ;
}
```
That's it — `useScenaroSession` handles identification, session start, transport connection, and
teardown. No transport object to wire up, nothing to render conditionally around it.
## What happens under the hood
The SDK identifies the visitor against the scenario and persists a JWT identity in `localStorage`.
A session is created for the scenario and the SDK opens a connection to the voice agent.
The SDK receives the scenario's `config` (features, tools, voice stack) and registers your
frontend tool handlers automatically.
`status` becomes `'connected'`, `agentState` reflects what the agent is doing
(`'listening' | 'thinking' | 'speaking'`), and `messages` accumulates the conversation.
## Reading state and handling messages
```tsx
const {
status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended'
agentState, // 'idle' | 'listening' | 'thinking' | 'speaking'
messages, // ConversationMessage[]
error, // ScenaroError | null
sendText,
setMicrophoneEnabled,
} = useScenaroSession({
scenario: SCENARIO_UUID,
onMessage: (message) => console.log(message),
onError: (error) => console.error(error.code, error.message),
});
```
## Frontend tools
Tools are plain functions — no request IDs, no manual response wrapping:
```tsx
useScenaroSession({
scenario: SCENARIO_UUID,
tools: {
search_products: async ({ query }) => searchCatalog(query),
open_page: async ({ url }) => {
router.push(url);
return { ok: true };
},
},
});
```
See [Authentication](/build/authentication) for caching, token refresh, and `external_id` behavior.
## Environment variables
| Variable | Example | Purpose |
|----------|---------|---------|
| `VITE_SCENARO_API_URL` | `https://api.scenaro.io` | API base URL (Vite apps) |
| `NEXT_PUBLIC_API_URL` | `https://api.scenaro.io/v1` | API base URL (Next.js — adjust `apiPathPrefix`) |
## Next steps
- [Implement an experience](/introduction/implement-an-experience) — full developer journey
- [Tools and features](/build/tools-and-features) — session tools vs FeatureProvider
- [Feature registry](/build/feature-registry) — wire tool calls to UI components
- [Embed voice on a website](/guides/recipes/embed-voice-website) — full production recipe
---
# Key concepts
Source: https://docs.scenaro.io/introduction/concepts/
Before diving into implementation details, understand how Scenaro pieces fit together.
## Scenario and publication
A **scenario** is the authored experience in Cockpit (prompt, features, collections, voice settings). A **publication** is the live UUID your frontend embeds. One scenario can have multiple publications (staging vs production).
Your app always uses the **publication UUID** — it's the `scenario` option you pass to `useScenaroSession`.
## Two-hop authentication
Scenaro separates **identity** from **session**:
| Step | Endpoint | Output |
|------|----------|--------|
| Identify | `POST /v1/public/token` | `user_token` (JWT), `user_id`, `application_id` |
| Session | `POST /v1/public/session` | **Transport credentials** — `livekit_token`, `livekit_url` (LiveKit is the current [Transport](/introduction/transport) implementation) |
The legacy single-call `generateToken` flow has been removed. Every integration must use identify then session — `useScenaroSession` runs both automatically when you call `start()`.
Identification is **idempotent on the client**: if a valid JWT is already in storage, no network call is made.
## Transport data channel
Once connected, the voice agent and frontend communicate over the **Transport data channel** —
implemented with LiveKit data topics today (see [Transport](/introduction/transport) for the
engine-agnostic boundary):
- `config` — agent sends feature definitions, collections, restore state
- `message:new` — conversation messages (user, assistant, tool, event)
- `tool:{toolName}` — dynamic tool response topics
- `state:update` — frontend sends experience checkpoints to the agent
`useScenaroSession` subscribes to these topics for you — you only see the results (`messages`, `config`, tool calls). See the [Protocol reference](/voice-agent/protocol/topics) if you're building on the raw channel.
## Feature registry
The SDK's `FeatureProvider` manages feature state (history, visibility, tool execution) but **does not ship business features**. Your app provides a `registry`:
```tsx
```
Each registry entry maps tool names to React components and handler functions. When the agent calls a tool, the SDK session dispatches the RPC to your handler. For tools without a UI panel, you can skip the registry and pass a plain function via the `tools` option of `useScenaroSession`.
## Event bus
`ScenaroProvider` creates a scoped `ScenaroEventBus` (replacing the legacy `window.__` global). Subscribe with `useScenaroEvents()` for config, messages, tool requests, and session events.
## Sub-exports mental model
```mermaid
flowchart TB
subgraph protocol ["@scenaro/sdk/protocol"]
Topics[topics.ts]
Messages[messages.ts]
RPC[rpc.ts]
end
subgraph client ["@scenaro/sdk/client"]
HTTP[ScenaroClient]
Storage[session-store]
end
subgraph react ["@scenaro/sdk/react"]
Provider[ScenaroProvider]
Session[useScenaroSession]
Features[FeatureProvider]
end
Agent[Voice Agent Python] --> Topics
HTTP --> Agent
Session --> HTTP
Session --> Topics
Features --> Session
```
| Layer | Use when |
|-------|----------|
| `protocol` | Defining cross-language contracts, agent development, type sharing |
| `client` | Node scripts, non-React apps, API calls without UI |
| `react` | Browser apps with voice sessions over Transport |
Non-React browser apps can use the headless `ScenaroSession` class from `@scenaro/sdk` — see the [Session reference](/sdk-reference/session).
## Session lifecycle
1. User calls `start()` → SDK identifies the visitor (cached JWT reused if valid)
2. SDK starts the session and connects the Transport → `status` becomes `'connecting'` then `'connected'`
3. Agent joins → sends `config` → SDK registers your tool handlers
4. Conversation runs → `messages` accumulates, tool RPCs fire your handlers
5. Agent or user ends → `onSessionEnd` fires, `status` becomes `'ended'`
## What stays in your app
- Feature UI components (search, cart, comparison)
- Brand styling and layout
- Collection-specific API calls
- Tool handler implementations (passed via `tools` or the feature registry)
---
# Transport
Source: https://docs.scenaro.io/introduction/transport/
`@scenaro/sdk` separates two things that most voice SDKs bundle together: the **session
contract** your app codes against, and the **realtime engine** that actually moves audio and
data. That boundary is called **Transport**.
You rarely need this page to ship a voice experience — [`useScenaroSession`](/sdk-reference/react)
hides Transport completely for the embed path. Read this if you're deciding whether code
belongs in your app or in a Transport-specific extension, or if you're curious how the SDK
stays engine-agnostic.
## Why a Transport boundary exists
Today, Transport is implemented with [LiveKit](https://livekit.io) — WebRTC audio plus a data
channel for the agent ↔ frontend protocol. Tomorrow it could be
[Pipecat](https://www.pipecat.ai) or another realtime engine. `ScenaroSession` never calls
LiveKit directly — it calls an internal `Transport` interface, and a `LiveKitTransport`
implements that interface underneath.
```mermaid
flowchart TB
subgraph portable ["Contract portable Scenaro"]
Session[ScenaroSession]
Protocol["Protocol - topics + RPC"]
end
subgraph transportLayer ["Transport abstraction"]
TransportIF[Transport interface]
end
subgraph engines ["Engine implementations"]
LK[LiveKitTransport]
PC["PipecatTransport (future)"]
end
Session --> TransportIF
Protocol --> Session
TransportIF --> LK
TransportIF -.-> PC
LK --> LKRoom["engine handle: Room"]
PC --> PCHandle["engine handle: TBD"]
```
This mirrors the internal boundary in the SDK source —
[`packages/sdk/src/core/transport.ts`](https://github.com/scenaro/projects/blob/main/packages/sdk/src/core/transport.ts):
```ts
/**
* Internal, non-public boundary between ScenaroSession's business logic and the real-time
* engine that carries it (LiveKit today, potentially another engine — e.g. Pipecat — later).
*/
export interface Transport {
connect(url: string, token: string): Promise;
disconnect(): Promise;
setMicrophoneEnabled(enabled: boolean): Promise;
sendText(text: string): Promise;
publishData(topic: string, data: Record): Promise;
registerToolHandler(name: string, handler: TransportToolRpcHandler): void;
onData(handler: (message: TransportDataMessage) => void): () => void;
onConnectionStateChange(handler: (state: TransportConnectionState) => void): () => void;
/** Non-portable escape hatch: the underlying engine object (e.g. LiveKit's `Room`). */
getEngineHandle(): unknown;
}
```
This interface is **not exported** from `@scenaro/sdk` — it's an internal seam, not a plugin
API you implement yourself. What it buys you as a developer is a stable contract on top of it.
## Two levels: portable contract vs engine extension
| Level | What it is | Stability | Examples |
|-------|-----------|-----------|----------|
| **Portable contract** | Scenaro's session API, independent of the engine underneath | Stable across future Transport implementations | `status`, `agentState`, `messages`, `config`, `tools`, session events |
| **Engine extension** | The current engine's own API surface | Tied to LiveKit today — not guaranteed to carry over | `room`, `RoomContext`, `@livekit/components-react` hooks/components |
Code written against the portable contract keeps working if Scenaro ever adds another Transport
implementation behind `useScenaroSession`. Code that calls `room.localParticipant...` or uses
LiveKit's React hooks is written against the current engine directly — the same trade-off you'd
make using that engine standalone.
```tsx
const { status, messages, room } = useScenaroSession({ scenario: PUBLICATION_UUID });
status; // portable — survives a future Transport swap
messages; // portable
room; // engine extension — LiveKit's Room, today
```
See [Advanced: Transport extensions](/guides/advanced-transport-integration) for how to use the
current engine's extension surface.
## What "transport session" means in the docs
Across the documentation, **transport session** refers to the realtime connection established
by `start()` — audio (if enabled) plus the data channel carrying the [protocol](/voice-agent/protocol/topics).
The wire response from `POST /v1/public/session` is described as **transport credentials**:
today those fields are named `livekit_token` and `livekit_url` (see
[`ScenaroClient.startSession`](/sdk-reference/client#startsession)), because LiveKit is the
current implementation. The field names are not renamed as part of this documentation
alignment — only the prose around them is engine-agnostic.
## Implementations
| Engine | Status | Where |
|--------|--------|-------|
| **LiveKit** | Current, only implementation | `packages/sdk/src/transport/livekit/` |
| **Pipecat** | Not implemented — this boundary exists so it could be added without rewriting `ScenaroSession` | — |
## Next steps
- [Advanced: Transport extensions](/guides/advanced-transport-integration) — using the LiveKit engine extension surface
- [Key concepts](/introduction/concepts) — scenario, auth, protocol
- [Protocol reference](/voice-agent/protocol/topics) — the data carried over Transport
---
# Coding agent support
Source: https://docs.scenaro.io/introduction/coding-agent-support/
Use Scenaro docs as context for AI coding assistants — the same pattern used by LiveKit, Retell, and Vapi.
Scenaro's SDK abstracts its realtime engine behind a [Transport](/introduction/transport)
boundary — LiveKit is the current implementation. Generated code should prefer the portable
`useScenaroSession` contract over engine-specific APIs unless the task explicitly needs
[Transport extensions](/guides/advanced-transport-integration).
## Quick links
| Resource | URL |
|----------|-----|
| Documentation index | https://docs.scenaro.io/llms.txt |
| Full documentation | https://docs.scenaro.io/llms-full.txt |
| MCP server | https://docs.scenaro.io/mcp |
| Markdown export | Append `.md` to any page URL |
## Instructions for AI agents
> Fetch the complete documentation index at: https://docs.scenaro.io/llms.txt
> Use this file to discover all available pages before exploring further.
>
> For clean Markdown of any page, append `.md` to the page URL.
>
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.scenaro.io/mcp
## Install the Scenaro skill
npx skills add https://docs.scenaro.io
## Contextual menu
Every documentation page includes a contextual menu (top-right) with:
- **Copy page** — copies the current page as Markdown
- **View as Markdown** — opens the `.md` version
- **Open in ChatGPT / Claude / Cursor** — starts a conversation with page context
- **MCP** — copies the MCP server URL
Press **Cmd+C** (Mac) or **Ctrl+C** (Windows) on any page to copy it as Markdown.
## Cursor setup
In Cursor Settings → MCP, add the Scenaro docs server:
```
https://docs.scenaro.io/mcp
```
Or use the contextual menu **Cursor** option on any page.
Paste `https://docs.scenaro.io/llms.txt` when you need the agent to discover all pages before drilling into specifics.
For large implementation tasks (full SDK integration, protocol work), provide `https://docs.scenaro.io/llms-full.txt` as a single context file.
## Recommended reading order for agents
When implementing a Scenaro integration, read these pages in order:
1. [Implement an experience](/introduction/implement-an-experience.md) — full journey map
2. [Quickstart](/introduction/quickstart.md) — minimal working embed
3. [Tools and features](/build/tools-and-features.md) — session tools vs FeatureProvider
4. [Authentication](/build/authentication.md) — identify → session flow
5. [Protocol topics](/voice-agent/protocol/topics.md) — data-channel contract
6. [SDK reference](/sdk-reference/react.md) — `useScenaroSession` API
7. [Transport](/introduction/transport.md) — only if the task touches engine-specific code
## Key facts for code generation
- Package: `@scenaro/sdk@0.3.0` on npm
- Legacy APIs (`useScenarioSession`, `AgentListener`, `getScenario`) were **removed in 0.3.0**
- Auth is **two-hop**: `identifyUser` then `startSession` — never a single token call
- Transport is abstracted (LiveKit today) — see [Transport](/introduction/transport). Only install `livekit-client >=2.0.0` and `@livekit/components-react >=2.0.0` for [Transport extensions](/guides/advanced-transport-integration)
- Storage keys: `scenaro_session:app:{applicationId}` + `scenaro_scenario_uuid`
- Business features are **not** in the SDK — inject via `FeatureProvider` registry
- Protocol source of truth: `packages/sdk/src/protocol/` in the monorepo
## Scenaro repositories
| Repo | Purpose |
|------|---------|
| [scenaro/projects](https://github.com/scenaro/projects) | Monorepo — `@scenaro/sdk`, experiences |
| [scenaro/platform-api](https://github.com/scenaro/platform-api) | Public and admin HTTP API |
| [scenaro/voice-agent](https://github.com/scenaro/voice-agent) | Python voice agent (LiveKit Agents today) |
| [scenaro/platform-spa](https://github.com/scenaro/platform-spa) | Cockpit and embed SPA |
---
# Changelog
Source: https://docs.scenaro.io/changelog/
## @scenaro/sdk
### 0.3.0
**Legacy API removed**
- Removed `useScenarioSession`, `AgentListener`, `useConversationMessages`, UI activity exports
- Removed `ScenaroClient.getScenario()` (endpoint never existed server-side)
- Added `rawConfig` on `ScenaroSession` / `useScenaroSession` for wire-format config access
- `SessionEventBridge` + `useFeatureToolRpc` replace legacy listener plumbing internally
- All experiences and platform-spa migrated to `useScenaroSession`
### 0.2.0
**New session contract — `useScenaroSession` and headless `ScenaroSession`**
- New single entry point: `useScenaroSession({ scenario, tools, onMessage, onError, onSessionEnd })` —
handles identification, session start, transport connection, config handshake, and tool RPC.
No `AgentListener` to mount, no manual `RoomContext` wiring.
- New framework-agnostic `ScenaroSession` class (`@scenaro/sdk`) with typed events
(`status-change`, `agent-state`, `message`, `config`, `session-end`, `error`).
- Frontend tools are plain functions `(args, context) => result` — request IDs, response
wrapping, and the RPC timeout (`TIMEOUT` + aborted `context.signal`) are handled by the SDK.
- Typed errors: `ScenaroError` with stable `code` values.
- Curated `SessionConfig` (camelCase) replaces raw wire payload exposure; `Collection` is now
fully typed in `@scenaro/sdk/protocol`.
- LiveKit isolated behind an internal transport boundary (lint-enforced); `room` stays
available as a documented LiveKit extension for advanced integrations.
- `ScenaroProvider` injects LiveKit `RoomContext` automatically and disposes the session on unmount.
**Breaking changes**
- Root entry (`@scenaro/sdk`) is now framework-agnostic: React exports moved exclusively to
`@scenaro/sdk/react`.
- Removed the empty `@scenaro/sdk/ui` sub-export.
- `startSession` no longer defaults `language` to `'fr-FR'` — omitting it uses the scenario's
configured language.
- `getAppSession`, `LEGACY_KEYS`, and `ApiFetchOptions` are no longer exported from
`@scenaro/sdk/client` (internal plumbing).
**Deprecations (removal in 0.3.0)**
- `useScenarioSession`, `AgentListener`, `useConversationMessages`, ui-activity-tracker
exports — see the [migration guide](/guides/migration-from-vendored-sdk).
- `ScenaroClient.getScenario()` — the backing endpoint is not implemented server-side.
### 0.1.1
**Simplified session storage**
- Identity stored in a single app-scoped blob: `scenaro_session:app:{applicationId}`
- Removed redundant flat keys (`scenaro_user_token`, `scenaro_user_id`, etc.)
- Automatic migration from legacy storage formats on read
- Legacy platform-spa keys (`user_token`, `external_id`) still supported via fallback
### 0.1.0
**Initial npm release**
- Package renamed from `@scenaro/embed-sdk` to `@scenaro/sdk`
- Sub-exports: `protocol`, `client`, `react`, `ui`
- Two-hop auth: `identifyUser` + `startSession`
- `ScenaroProvider`, `useScenarioSession`, `AgentListener`
- `FeatureProvider` with injectable registry
- Scoped `ScenaroEventBus` (replaces `window.__`)
- LiveKit peer dependencies (`>=2.0.0`)
- Protocol: data-channel topics, message types, RPC contracts
## Documentation
### 2026-07-11
**SDK 0.3.0 documentation alignment**
- New [Implement an experience](/introduction/implement-an-experience) journey map
- New [Tools and features](/build/tools-and-features) — session tools vs FeatureProvider
- Removed stale legacy API references (`useScenarioSession`, `AgentListener`, `getScenario`)
- Version pins updated to `@scenaro/sdk@0.3.0`
- OpenAPI: removed unimplemented `GET /public/scenarios/{uuid}`
**Transport terminology**
- New [Transport](/introduction/transport) page — the engine-agnostic boundary behind
`ScenaroSession` (LiveKit today, extensible to other engines such as Pipecat)
- Renamed `guides/advanced-livekit-integration` to
[Advanced: Transport extensions](/guides/advanced-transport-integration) (redirect kept)
- Documentation now distinguishes the **portable contract** (`status`, `messages`, `config`,
`tools`) from **engine extensions** (`room`, `RoomContext`) everywhere — install instructions,
diagrams, and reference tables no longer imply LiveKit is required for the embed path
### 2026-07-10
**Initial public documentation**
- Mintlify site at [docs.scenaro.io](https://docs.scenaro.io)
- AI-ready: `llms.txt`, `.md` export, MCP server, contextual menu
- Guides: quickstart, auth, protocol, feature registry, 5 recipes
- SDK reference for `client`, `react`, `protocol`, `session`
- Public API OpenAPI spec (initial)
## Reporting issues
- SDK bugs: [github.com/scenaro/projects/issues](https://github.com/scenaro/projects/issues)
- Documentation: same repo, label `documentation`
---
# Cockpit
Source: https://docs.scenaro.io/build/cockpit/
Cockpit is Scenaro's authoring environment. Use it to design voice scenarios, configure features and collections, test sessions, and publish to production.
## Workflow
Define the agent prompt, voice stack, language, and behavior in Cockpit.
Attach features (product search, display, comparison, bucket list, etc.) and link them to tools the agent can call.
Bind product or content collections so features can query your catalog data.
Use the live test page (`/scenarios/[id]/live`) or the public embed preview to validate voice and tool behavior.
Publish the scenario to obtain a **publication UUID** for production embeds.
## What you get after publishing
| Artifact | Used by |
|----------|---------|
| Publication UUID | Frontend `useScenaroSession({ scenario })` |
| Feature config | Sent by agent on `config` data topic at session start |
| Collections metadata | Passed to feature handlers and UI components |
## Testing before production
- **Cockpit live page** — internal test with full feature registry (platform-spa)
- **Public embed** — `/public/[uuid]` route for stakeholder review
- **Experience apps** — brand-specific Vite apps in the monorepo (`experiences/*`)
## Environment alignment
Ensure your test and production frontends point to the correct API:
| Environment | API URL |
|-------------|---------|
| Local dev | `https://localhost:8443` or bootstrap default |
| Staging | Your staging API host |
| Production | `https://api.scenaro.io` |
## Next steps
- [React integration](/build/react-integration) — wire the publication UUID in your app
- [Feature registry](/build/feature-registry) — map Cockpit features to frontend components
---
# React integration
Source: https://docs.scenaro.io/build/react-integration/
This guide covers the standard React integration pattern used across Scenaro experience apps and platform-spa.
## Provider stack
```tsx
import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react';
export function ScenaroLiveProviders({ children, registry }) {
return (
{children}
);
}
```
## ScenaroProvider
Owns the session, creates a shared `ScenaroClient` and scoped `ScenaroEventBus`, and injects the current [Transport](/introduction/transport) implementation's `RoomContext` (LiveKit today) silently — no manual `RoomContext.Provider` wiring needed.
| Prop | Type | Description |
|------|------|-------------|
| `client` | `ScenaroClient` | Pre-built client instance (preferred for shared config) |
| `apiUrl` | `string` | API base URL (alternative to `client`) |
| `apiPathPrefix` | `string` | Path prefix, default `/v1` |
| `storagePrefix` | `string` | Prefix for localStorage keys (multi-tenant embeds) |
| `storage` | `StorageAdapter \| null` | Custom storage (default: `localStorage`) |
Hooks:
- `useScenaro()` — full context (`client` + `events`)
- `useScenaroClient()` — HTTP client only
- `useScenaroEvents()` — event bus only
## useScenaroSession
The single entry point for a session. It handles the full lifecycle — identify, start session, Transport connect/disconnect — plus the data channel, config handshake, and tool RPC internally.
```tsx
import { useScenaroSession } from '@scenaro/sdk/react';
const {
status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended'
agentState, // 'idle' | 'listening' | 'thinking' | 'speaking'
messages, // ConversationMessage[]
config, // SessionConfig | null (curated)
rawConfig, // ConfigPayload | null (wire format)
conversationId,
error, // ScenaroError | null
start, // (overrides?) => Promise
end, // () => Promise
sendText, // (text) => Promise
setMicrophoneEnabled,
} = useScenaroSession({
scenario: 'publication-uuid',
onMessage: (m) => console.log(m),
onError: (e) => console.error(e.code),
onSessionEnd: (info) => {},
});
```
### Options
| Option | Description |
|--------|-------------|
| `scenario` | Publication UUID (required, cannot be overridden in `start`) |
| `language` | Optional BCP-47 tag; omit to use the scenario's configured language |
| `tools` | Frontend tool handlers (see below) |
| `onMessage` | Called for each `ConversationMessage` |
| `onError` | Called with a `ScenaroError` (has `.code`) |
| `onSessionEnd` | Called when the session ends |
`start(overrides?)` accepts option overrides for the session being started — everything except `scenario`.
### Frontend tools
Tools are plain async functions — no `requestId` plumbing:
```tsx
useScenaroSession({
scenario: 'publication-uuid',
tools: {
search_products: async (args, context) => {
// context: { toolName, settings, collections: Collection[], signal: AbortSignal }
return await searchCatalog(args, { signal: context.signal });
},
},
});
```
### Session config
After the config handshake:
- `config` — curated `{ features, collections, avatarEnabled, audioDictationEnabled }`
- `rawConfig` — full wire payload (`restore_state`, tool wiring, snake_case fields)
See [Tools and features](/build/tools-and-features#config-curated-vs-raw) for when to use each.
### Advanced: engine handle access
The hook also returns `room` — the current Transport implementation's engine handle (LiveKit
`Room | null`) — for advanced use cases. Most apps never need it — see
[Advanced: Transport extensions](/guides/advanced-transport-integration).
The hook is powered by the headless `ScenaroSession` class from `@scenaro/sdk`, usable outside React. See the [ScenaroSession reference](/sdk-reference/session).
## Next.js
Add the SDK to `transpilePackages`:
```js
// next.config.js
module.exports = {
transpilePackages: ['@scenaro/sdk'],
};
```
## Next steps
- [Tools and features](/build/tools-and-features)
- [Authentication](/build/authentication)
- [Events and listeners](/build/events-and-listener)
- [Feature registry](/build/feature-registry)
---
# Authentication
Source: https://docs.scenaro.io/build/authentication/
Scenaro uses a **two-step authentication flow**. Identity (JWT) is separate from the Transport
session credentials.
If you use `useScenaroSession` (React) or `ScenaroSession` (headless), both steps run automatically when you call `start()`. This page documents the underlying client flow.
## Flow diagram
```mermaid
sequenceDiagram
participant App as Your App
participant API as Platform API
participant Transport as Transport (LiveKit today)
App->>API: POST /v1/public/token (identifyUser)
API-->>App: user_token, user_id, application_id
App->>API: POST /v1/public/session (Bearer user_token)
API-->>App: transport credentials (livekit_token, livekit_url)
App->>Transport: connect(url, token)
```
## Step 1 — Identify
```ts
import { createScenaroClient } from '@scenaro/sdk/client';
const client = createScenaroClient({
apiUrl: 'https://api.scenaro.io',
apiPathPrefix: '/v1',
});
const identity = await client.identifyUser(scenarioUUID, externalId);
// { user_token, user_id, application_id }
```
**Request body** (`POST /v1/public/token`):
```json
{
"scenario_uuid": "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7",
"external_id": "guest-abc123"
}
```
### Client-side caching
`identifyUser` is **idempotent**: if a non-expired `user_token` with `user_id` and `application_id` exists in storage, it returns the cached identity without a network call.
### external_id
If you omit `externalId`, the SDK generates a stable anonymous ID and persists it. This consolidates conversations for returning visitors without login.
## Step 2 — Start session
```ts
const session = await client.startSession(scenarioUUID, {
inputMode: 'audio',
outputMode: 'audio',
language: 'en-US',
sessionOptions: {
resumeConversationId: 'conv-uuid',
metadata: { page: '/products' },
},
});
// session.livekit_url / session.livekit_token are the Transport credentials —
// named after LiveKit, the current Transport implementation. See /introduction/transport.
await transport.connect(session.livekit_url, session.livekit_token);
```
`language` is optional — omit it and the backend applies the scenario's configured language.
**Request body** (`POST /v1/public/session`):
```json
{
"scenario_uuid": "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7",
"input_mode": "audio",
"output_mode": "audio",
"language": "en-US"
}
```
Requires `Authorization: Bearer {user_token}`.
## Token refresh outside sessions
Features that call the API outside the Transport session (search, cart) use:
```ts
const token = await client.getValidUserToken();
```
This returns the stored token if valid, otherwise silently re-identifies using persisted `scenario_uuid` and `external_id`.
## JWT expiration
Tokens are validated locally with a **90-second buffer** (`EXPIRY_BUFFER_SECONDS`). No server round-trip is needed to check expiration.
```ts
import { isTokenExpired, getJWTExpiry } from '@scenaro/sdk/client';
```
## Deprecated flow
The legacy `generateToken` single-call flow (one request creating user + session) has been removed. Do not implement it in new integrations.
## Clear session
```ts
client.clearStoredSession(scenarioUUID);
```
Removes the app-scoped identity blob and scenario pointer. See [Session storage](/build/session-storage) for key details.
## API reference
See the [API Reference](/api-reference) tab for full request/response schemas.
---
# Session storage
Source: https://docs.scenaro.io/build/session-storage/
After `identifyUser`, the SDK persists identity in browser storage using a simplified model introduced in `@scenaro/sdk@0.1.1`.
## Storage keys
| Key | Content |
|-----|---------|
| `scenaro_session:app:{applicationId}` | **Source of truth** — JSON blob `{ user_token, user_id, application_id, external_id }` |
| `scenaro_scenario_uuid` | Pointer to the active publication UUID (for token refresh context) |
Example after identify:
```
scenaro_session:app:2 → {"external_id":"guest-…","user_token":"eyJ…","user_id":64,"application_id":2}
scenaro_scenario_uuid → "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7"
```
Transport session credentials (LiveKit tokens today) are **ephemeral** — they are not stored. Only the JWT identity is persisted.
## Removed keys (v0.1.1+)
The following are no longer written at identify time:
- Flat keys: `scenaro_user_token`, `scenaro_user_id`, `scenaro_application_id`, `scenaro_external_id`
- Duplicate scenario blob: `scenaro_session:{scenarioUUID}`
## Automatic migration
On read, the SDK client consolidates legacy storage formats (flat keys or scenario-scoped blobs)
into `scenaro_session:app:{id}` and deletes obsolete keys. Legacy platform-spa keys
(`user_token`, `external_id`) are still supported once via fallback.
## Custom storage
Inject a `StorageAdapter` for SSR, tests, or non-browser environments:
```ts
const client = createScenaroClient({
apiUrl: 'https://api.scenaro.io',
storage: myMockStorage, // or null for SSR
});
```
```ts
interface StorageAdapter {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
```
Default: `localStorage` in browser, `null` on server.
## Multi-embed prefix
For multiple Scenaro embeds on one origin, use `storagePrefix`:
```ts
createScenaroClient({
apiUrl: 'https://api.scenaro.io',
storagePrefix: 'brand_a',
});
// Keys become: brand_a_scenaro_session:app:2
```
## Reading stored values
```ts
client.getStoredUserToken(scenarioUUID?, applicationId?);
client.getSessionUserId();
client.getSessionApplicationId();
```
## Clear session
```ts
// Clear specific scenario context
client.clearStoredSession(scenarioUUID);
// Clear all Scenaro storage for this prefix
client.clearStoredSession();
```
---
# Tools and features
Source: https://docs.scenaro.io/build/tools-and-features/
Scenaro voice agents call **frontend tools** over Transport RPC (LiveKit RPC today — see
[Transport](/introduction/transport)). The SDK offers two ways to handle those calls. This page
explains when to use each — similar to how Vapi separates
[client-side tools](https://docs.vapi.ai/tools/client-side-websdk) (browser effects) from
server tools (model needs the result).
Do not register the same RPC method name in both patterns — behavior will conflict.
## Decision tree
```mermaid
flowchart TD
A[Agent calls a frontend tool] --> B{Does the tool open UI?}
B -->|No — fetch data, update state silently| C[Session tools]
B -->|Yes — search panel, product card, cart| D{Using SDK FeatureProvider?}
D -->|Yes| E[FeatureProvider registry]
D -->|No — vendored FeatureManager| F[Local AgentListener + FeatureManager]
C --> G["useScenaroSession({ tools })"]
E --> H["FeatureProvider({ registry })"]
F --> I["engine handle registerRpcMethod → executeTool"]
```
## Pattern 1 — Session tools
**Use when:** the handler returns data to the agent and does not need a feature panel, history,
or overlay UI.
**Registration:** pass `tools` to `useScenaroSession` (or `session.start({ tools })` headless).
**Handler signature:**
```ts
type ToolHandler = (
args: Record,
context: {
toolName: string;
settings: Record;
collections: Collection[];
signal: AbortSignal;
},
) => unknown | Promise;
```
```tsx
useScenaroSession({
scenario: PUBLICATION_UUID,
tools: {
get_store_hours: async (_args, context) => {
return { open: '9:00', close: '18:00', timezone: context.settings.timezone };
},
},
});
```
The SDK:
- Registers RPC methods on connect
- Wraps your return value as `{ requestId, code: 'OK', data }`
- Aborts `context.signal` after 10 seconds (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`)
- Returns `{ code: 'TIMEOUT' }` to the agent on timeout
## Pattern 2 — FeatureProvider tools
**Use when:** the tool drives a UI feature panel — search results, product detail, comparison,
shopping list — with history, visibility, and component rendering.
**Registration:** `FeatureProvider` with a `Map` keyed by **feature type**
(e.g. `collection-items-search`).
**Handler signature** (different from session tools):
```ts
type FeatureToolHandler = (
requestId: string,
args: Record,
settings?: Record,
collections?: Collection[],
) => ToolExecutionResult;
```
```tsx
const registry = new Map([
['collection-items-search', {
toolNames: ['collection:items:search'],
primaryTool: 'collection:items:search',
component: SearchPanel,
handlers: {
'collection:items:search': async (requestId, args, settings, collections) => {
const results = await searchAPI(args, collections);
return { success: true, data: results, requestId };
},
},
}],
]);
{children}
```
`FeatureProvider` internally mounts `useFeatureToolRpc`, which:
1. Listens for `config` on the event bus
2. Registers RPC methods for tools listed in the wire config
3. Emits `tool:execute` → runs `executeTool` → responds to the agent
See [Feature registry](/build/feature-registry) and [custom feature recipe](/guides/recipes/custom-feature).
## Local FeatureManager vs SDK FeatureProvider
Both patterns exist in the Scenaro monorepo:
| | SDK `FeatureProvider` | Local `FeatureManager` |
|--|----------------------|------------------------|
| **Used by** | platform-spa live page | All 7 experience apps |
| **Session hook** | `useScenaroSession` | `useScenaroSession` (via thin adapter) |
| **Tool RPC** | SDK `useFeatureToolRpc` | Local `AgentListener` → engine handle `registerRpcMethod` |
| **Feature state** | SDK `useFeatures()` | Local `useFeatures()` in vendored runtime |
| **When to choose** | Greenfield app, no vendored copy | Migrating existing experience with local runtime |
Experience apps typically keep a thin adapter at `src/lib/scenaro-sdk/hooks/useScenarioSession.js`
that maps `connect` → `start()`, `isConnected` → `status === 'connected'`, etc. The local
`FeatureManager` + `AgentListener` handle tool RPC independently of SDK `FeatureProvider`.
You do **not** need to migrate to SDK `FeatureProvider` unless you want to drop the vendored
feature runtime.
## Config: curated vs raw
After the config handshake you get two views:
| Field | Type | Use for |
|-------|------|---------|
| `config` | `SessionConfig` | Features list, collections, avatar/dictation flags |
| `rawConfig` | `ConfigPayload` | `restore_state`, tool wiring, snake_case backend fields |
```tsx
const { config, rawConfig } = useScenaroSession({ scenario: PUBLICATION_UUID });
// Curated — most UI
config?.features.map(f => f.type);
// Wire format — state restoration, debugging
rawConfig?.restore_state;
```
On the event bus, `events.on('config', …)` receives the **raw** `ConfigPayload`. On the session,
`session.on('config', …)` receives the **curated** `SessionConfig`.
## RPC flow (both patterns)
```mermaid
sequenceDiagram
participant Agent as Voice agent
participant Transport as Transport RPC
participant SDK as SDK Transport layer
participant Handler as Your handler
Agent->>Transport: RPC (tool name, args)
Transport->>SDK: registered method
SDK->>Handler: invoke handler
Handler-->>SDK: result
SDK-->>Agent: JSON { requestId, code, data }
```
## Topics not exposed as SDK helpers
Some protocol topics exist but have no high-level SDK API yet. Use the engine handle (`room`)
+ manual data listeners if needed:
| Topic | Purpose |
|-------|---------|
| `assistant:interrupt` | Stop agent speech |
| `state:update` | Session checkpoints |
| `dictation:transcript` | STT partials |
| `event:fire` | Custom analytics events |
See [Protocol topics](/voice-agent/protocol/topics).
## Next steps
- [Feature registry](/build/feature-registry) — registry shape and `useFeatures()` API
- [Events and listeners](/build/events-and-listener) — event bus catalog
- [Product search recipe](/guides/recipes/product-search) — full feature example
- [SDK reference: react](/sdk-reference/react) — `ToolHandler` types
---
# Feature registry
Source: https://docs.scenaro.io/build/feature-registry/
Not sure whether to use `FeatureProvider` or session tools? Read
[Tools and features](/build/tools-and-features) first.
`FeatureProvider` manages feature state (history, visibility, tool execution) but **ships no business features**. Your app provides a registry that maps agent tool names to React components and handlers.
## Architecture
```mermaid
flowchart LR
Agent[Voice Agent] -->|tool RPC| Session[useScenaroSession]
Session -->|tool:request| Bus[ScenaroEventBus]
Bus --> Runtime[FeatureProvider]
Runtime -->|executeTool| Handler[Your handler]
Handler --> Component[Your React component]
```
## Basic setup
```tsx
import { FeatureProvider } from '@scenaro/sdk/react';
import { SearchFeature } from './features/search';
import { searchHandler } from './features/search-handler';
const FEATURE_REGISTRY = new Map([
['collection:items:search', {
component: SearchFeature,
handlers: { search_collection_items: searchHandler },
primaryTool: 'search_collection_items',
toolNames: ['search_collection_items'],
}],
]);
bucketStorage.resetAll()}
clearHistoryOnSessionEnd={false}
initialVisibility={true}
initialPosition="right"
>
{children}
```
## ToolRegistryEntry
| Field | Type | Description |
|-------|------|-------------|
| `component` | `React.ComponentType` | UI rendered when the feature is active |
| `handlers` | `Record` | Async functions called on tool RPC |
| `primaryTool` | `string` | Main tool name for this feature |
| `toolNames` | `string[]` | All tool names that map to this feature |
| `overlay` | `boolean` | Render as overlay vs inline panel |
| `propsSelector` | `function` | Map tool state → component props |
## Tool handler signature
```ts
type ToolHandler = (
requestId: string,
args: unknown,
settings?: Record,
collections?: unknown[],
) => Promise | ToolExecutionResult;
```
Return shape:
```ts
{
requestId: string;
response: {
code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED';
data: unknown; // sent back to the agent
};
componentData?: Record; // updates UI state
}
```
## Config handshake
When the agent joins, it sends a `config` message with `features` and `collections`. `FeatureProvider`:
1. Registers available features from the config
2. Maps tool names to registry entries
3. The session (`useScenaroSession`) registers Transport RPC methods for each tool
For simple one-off tools that don't need feature UI, you can skip the registry and pass plain functions via the `tools` option of `useScenaroSession`. See [React integration](/build/react-integration).
## Common feature IDs
| Feature ID | Description |
|------------|-------------|
| `collection:items:search` | Search and explore catalog items |
| `collection:items:display` | Product detail sheet |
| `collection:items:comparison` | Side-by-side comparison |
| `bucket:list:crud` | Basket / list management |
| `workflow:conversation:end` | End conversation, redirect |
Feature IDs come from Cockpit configuration. Your registry keys must match.
## What stays outside the SDK
Per the architecture decision (July 2026):
- Feature UI components (too brand-specific)
- Collection API clients
- Product templates and styling
- Custom `FeatureManager` wrappers (legacy — migrate to `FeatureProvider`)
## Recipes
- [Custom feature](/guides/recipes/custom-feature)
- [Product search](/guides/recipes/product-search)
- [E-commerce embed](/guides/recipes/ecommerce-embed)
---
# Events and listeners
Source: https://docs.scenaro.io/build/events-and-listener/
`useScenaroSession` handles the [Transport](/introduction/transport) data channel, config
handshake, and tool RPC internally. You consume events through hook callbacks, returned state,
and the scoped event bus.
## Session events via the hook
```tsx
import { useScenaroSession } from '@scenaro/sdk/react';
const { status, agentState, messages, config, rawConfig } = useScenaroSession({
scenario: 'publication-uuid',
onMessage: (m) => console.log('message', m),
onError: (e) => console.error(e.code),
onSessionEnd: (info) => showSummary(info),
});
```
| Field | Description |
|-------|-------------|
| `messages` | Accumulated conversation — no separate hook needed |
| `config` | Curated session config after handshake |
| `rawConfig` | Full wire `ConfigPayload` (`restore_state`, tool wiring) |
| `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` |
| `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` |
Outside React, the headless `ScenaroSession` class emits the same events: `status-change`,
`agent-state`, `message`, `config`, `session-end`, `error`. See the
[ScenaroSession reference](/sdk-reference/session).
## Event bus
`ScenaroProvider` creates one `ScenaroEventBus` per provider tree. Subscribe with
`useScenaroEvents()`:
```tsx
import { useEffect } from 'react';
import { useScenaroEvents } from '@scenaro/sdk/react';
function DebugPanel() {
const events = useScenaroEvents();
useEffect(() => {
const onConfig = (payload) => console.log('config', payload);
events.on('config', onConfig);
return () => events.off('config', onConfig);
}, [events]);
return null;
}
```
### Event catalog
| Event | Emitter | Payload |
|-------|---------|---------|
| `config` | `SessionEventBridge` | Raw `ConfigPayload` |
| `message:new` | `SessionEventBridge` | `ConversationMessage` |
| `session:end` | `SessionEventBridge` | `{ reason: string }` |
| `tool:execute` | `useFeatureToolRpc` | `{ toolName, requestId, args }` |
| `tool:response` | `FeatureProvider` | `{ requestId, response }` |
| `tool:error` | `FeatureProvider` | `{ requestId, toolName, error }` |
| `feature:show` | `FeatureProvider` | `{ reason, toolName, requestId }` |
| `suggestions:show` | `FeatureProvider` | `{ suggestions }` |
| `conversation:reset` | Your app (manual) | — triggers `FeatureProvider.reset()` |
`session.on('config')` delivers the **curated** `SessionConfig`. `events.on('config')` delivers
the **raw** wire payload. See [Tools and features](/build/tools-and-features#config-curated-vs-raw).
## Tool RPC flow
For **session tools**, declare plain functions on `useScenaroSession`:
```mermaid
sequenceDiagram
participant Agent as Voice Agent
participant Transport as Transport RPC
participant SDK as useScenaroSession
participant Handler as Your tool function
Agent->>Transport: RPC call (tool name)
Transport->>SDK: registered RPC method
SDK->>Handler: tools[toolName](args, context)
Handler-->>SDK: result
SDK-->>Agent: JSON response
```
For **feature tools** via `FeatureProvider`, see [Tools and features](/build/tools-and-features).
RPC timeout for frontend tools: **10 seconds** (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`). The
`context.signal` (`AbortSignal`) is aborted on timeout or session end.
## Text input
Send user text with `sendText(text)` from the hook. It publishes on the `lk.chat` topic via
the Transport data channel.
## Session end detection
The SDK ends the session on:
- Transport disconnected event
- Agent participant leaving (2s grace period)
- `session:end` data topic
`status` becomes `'ended'` and `onSessionEnd` fires with the end payload.
## Pre-0.3 migration note
`useScenarioSession`, `AgentListener`, and `useConversationMessages` were removed in
**0.3.0**. Use `useScenaroSession` for session lifecycle and `messages` for conversation UI.
See the [migration guide](/guides/migration-from-vendored-sdk).
---
# Voice agent architecture
Source: https://docs.scenaro.io/voice-agent/architecture/
The Scenaro voice agent is a Python service built on [LiveKit Agents](https://docs.livekit.io/agents/) — the current [Transport](/introduction/transport) implementation. It joins the Transport session, runs the voice pipeline (STT → LLM → TTS), and communicates with the frontend over the data channel.
## Repository
Source: [github.com/scenaro/voice-agent](https://github.com/scenaro/voice-agent)
Local development is bootstrapped from the [projects workspace](https://github.com/scenaro/projects) via `./bootstrap-scenaro-local.sh`.
## Session lifecycle
```mermaid
sequenceDiagram
participant FE as Frontend SDK
participant API as Platform API
participant Transport as Transport (LiveKit today)
participant Agent as Voice Agent
FE->>API: startSession
API->>Transport: dispatch agent job
FE->>Transport: connect (WebRTC)
Agent->>Transport: join session
Agent->>FE: config (data channel)
FE->>Agent: config_ack
loop Conversation
Agent->>FE: message:new
Agent->>FE: RPC tool call
FE->>Agent: RPC tool response
end
Agent->>FE: session:end
```
## Responsibilities
| Layer | Responsibility |
|-------|----------------|
| **Platform API** | Session creation, agent dispatch, scenario config loading |
| **Voice Agent** | Voice pipeline, LLM reasoning, tool selection, protocol messages |
| **@scenaro/sdk** | Data channel listen, tool RPC handlers, feature UI |
| **Transport (LiveKit today)** | WebRTC realtime engine, session/room management |
## Config handshake
On join, the agent sends a `config` payload:
```json
{
"type": "config",
"features": [{ "name": "search", "type": "collection:items:search", "tools": [...] }],
"collections": [...],
"company_id": 2,
"restore_state": { "checkpoint": "...", "data": {} }
}
```
The frontend responds with `config_ack`. Until ack is received, the agent may wait or retry with `config_request`.
## Tool execution model
Tools fall into two categories:
| Type | Executed by | Path |
|------|-------------|------|
| Backend tools | Voice agent | Internal Python functions |
| Frontend tools | Your React app | Transport RPC (LiveKit today) → SDK session → your tool handler |
Frontend tools declare a `frontend_method` in Cockpit. The agent calls it via Transport RPC; the SDK session routes it to your handler — a `tools` entry on `useScenaroSession` or a `FeatureProvider` registry handler.
## State sync
The frontend can send experience checkpoints on `state:update`. The agent uses this for context restoration across page navigations or session resumes.
## Voice stack
Configured per scenario in Cockpit. The agent supports multiple STT/LLM/TTS providers via LiveKit Agents plugins (the current Transport implementation's agent framework).
## Related docs
- [Protocol topics](/voice-agent/protocol/topics)
- [Protocol messages](/voice-agent/protocol/messages)
- [RPC contracts](/voice-agent/protocol/rpc)
- [Prompting guide](/voice-agent/prompting)
---
# Prompting guide
Source: https://docs.scenaro.io/voice-agent/prompting/
Effective prompts make the difference between a voice experience that feels natural and one that confuses users. This guide covers patterns that work well with Scenaro's feature + tool architecture.
## Core principles
1. **One job per turn** — ask one question or request one action at a time
2. **Tool-aware instructions** — tell the agent when to call each tool and what to say while waiting
3. **Speakable output** — write responses meant to be heard, not read
4. **Feature context** — reference what the user sees on screen when features are active
## Structure your system prompt
```
## Role
You are a helpful shopping assistant for [Brand].
## Capabilities
- Search the product catalog
- Show product details
- Compare items
- Manage the shopping list
## Tool usage
- When the user asks to find products, call search_collection_items
- When showing a product, call display_collection_item
- Keep speaking while tools execute: "Let me look that up for you"
## Conversation style
- Short sentences, conversational tone
- Confirm understanding before complex actions
- Offer alternatives when search returns no results
```
## Tool call narration
Users hear silence during tool execution unless the agent speaks. Instruct the agent to:
- Announce what it's doing: *"I'm searching for red wines under 30 euros"*
- Summarize results concisely: *"I found 5 options, the first is…"*
- Handle empty results gracefully: *"I didn't find an exact match — want me to broaden the search?"*
## Feature-aware prompts
When features render UI panels, tell the agent to reference them:
```
When search results appear on screen, refer to them as "the results I'm showing you"
rather than reading out every item. Highlight the top 2-3 verbally.
```
## Language and locale
Set the scenario language in Cockpit and pass the same BCP-47 tag from the frontend:
```tsx
const { start } = useScenaroSession({ scenario: PUBLICATION_UUID, language: 'fr-FR' });
// or pass language in start({ language: 'fr-FR' })
```
Keep prompt language consistent with the session language.
## State restoration
If your experience uses `state:update` checkpoints, include restoration instructions:
```
If restore_state is provided, acknowledge what the user was doing:
"Welcome back — you were looking at the Bordeaux selection."
```
## Testing prompts
1. Test in Cockpit live page with real feature registry
2. Verify tool calls trigger the correct frontend RPC
3. Check behavior on timeout (slow network, empty results)
4. Test interruption — user speaks while agent is talking
## Anti-patterns
| Avoid | Why |
|-------|-----|
| Long lists read aloud | Users can't scan audio — use UI features |
| Calling tools without narration | Creates awkward silence |
| Assuming tool success | Always handle ERROR and empty results |
| Mixing languages | Mismatched STT/TTS/prompt languages degrade quality |
---
# Data channel topics
Source: https://docs.scenaro.io/voice-agent/protocol/topics/
Topics are defined in `@scenaro/sdk/protocol` and shared between the Python voice agent and all
frontends. They travel over the [Transport](/introduction/transport) data channel — LiveKit's
data channel today.
```ts
import { DATA_TOPICS, toolDataTopic } from '@scenaro/sdk/protocol';
```
## Topic table
| Topic | Direction | Role |
|-------|-----------|------|
| `config` | agent → front | Handshake: features, collections, restore state |
| `config_ack` | front → agent | Acknowledge config received |
| `config_request` | agent → front | Request config resend |
| `message:new` | agent → front | Conversation message (user/assistant/tool/event) |
| `session:end` | agent → front | Agent-initiated session end |
| `dictation:transcript` | agent → front | Real-time STT transcript |
| `state:update` | front → agent | Experience state checkpoint |
| `ui:activity` | front → agent | UI activity heartbeat |
| `assistant:interrupt` | front → agent | Interrupt assistant speech |
| `session:control` | bidirectional | Session control signals |
| `event:fire` | bidirectional | Custom event firing |
| `lk.chat` | front → agent | User text message |
| `tool:{toolName}` | dynamic | Tool response channel via `toolDataTopic()` |
## Source definition
```ts
export const DATA_TOPICS = {
CONFIG: 'config',
CONFIG_ACK: 'config_ack',
CONFIG_REQUEST: 'config_request',
MESSAGE_NEW: 'message:new',
SESSION_END: 'session:end',
DICTATION_TRANSCRIPT: 'dictation:transcript',
EVENT_FIRE: 'event:fire',
STATE_UPDATE: 'state:update',
SESSION_CONTROL: 'session:control',
ASSISTANT_INTERRUPT: 'assistant:interrupt',
UI_ACTIVITY: 'ui:activity',
CHAT: 'lk.chat',
} as const;
```
## Dynamic tool topics
```ts
toolDataTopic('search_collection_items'); // → 'tool:search_collection_items'
```
## Handshake sequence
Agent publishes on `config` with features, collections, and optional `restore_state`.
The SDK session dispatches the payload on the event bus; `FeatureProvider` registers features and RPC methods.
Frontend sends `config_ack` with `{ acknowledged: true, timestamp }`.
## Versioning
The protocol package has **zero dependencies** and serves as the cross-language spec. Future Python/Go SDKs will align on this same topic and message definitions.
Potential future extraction: standalone `scenaro-protocol` npm/PyPI package.
## See also
- [Message types](/voice-agent/protocol/messages)
- [RPC methods](/voice-agent/protocol/rpc)
---
# Message types
Source: https://docs.scenaro.io/voice-agent/protocol/messages/
All message types are exported from `@scenaro/sdk/protocol`.
```ts
import type {
ConfigPayload,
ConversationMessage,
PublicSessionResponse,
} from '@scenaro/sdk/protocol';
```
## ConfigPayload
Sent on the `config` topic at session start.
| Field | Type | Description |
|-------|------|-------------|
| `features` | `FeatureDefinition[]` | Features and their tools |
| `collections` | `unknown[]` | Collection metadata for features |
| `company_id` | `string \| number` | Tenant identifier |
| `restore_state` | `RestoreState` | Checkpoint for state restoration |
| `voice_stack` | `string` | Active voice provider stack |
| `audio_dictation` | `boolean` | Dictation mode enabled |
| `conversation_id` | `string` | Active conversation ID |
| `avatar_enabled` | `boolean` | Avatar output enabled |
### FeatureDefinition
```ts
interface FeatureDefinition {
name: string | Record;
type: string; // e.g. 'collection:items:search'
settings: Record;
tools: ToolConfig[];
}
```
### ToolConfig
```ts
interface ToolConfig {
name: string;
type: string;
settings?: Record;
frontend_method?: string; // Transport RPC method name override (LiveKit today)
rpc_timeout_s?: number;
}
```
## ConfigAckPayload
```ts
interface ConfigAckPayload {
acknowledged: boolean;
timestamp: number;
suppress_recap?: boolean;
}
```
## ConversationMessage
Sent on `message:new`.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Message ID |
| `role` | `'user' \| 'assistant' \| 'tool' \| 'event'` | Speaker role |
| `content` | `string` | Message text |
| `timestamp` | `string` | ISO timestamp |
| `tool_calls` | `unknown[]` | Tool invocations (assistant) |
| `tool_call_id` | `string` | Tool result reference |
| `interrupted` | `boolean` | Speech was interrupted |
| `experience_state` | `Record` | UI state snapshot |
## SessionEndPayload
```ts
interface SessionEndPayload {
reason?: string;
ended_at?: string;
}
```
## API types (client)
Used by `ScenaroClient` for HTTP calls:
### PublicTokenRequest / Response
```ts
// Request
{ external_id: string; scenario_uuid: string }
// Response
{ user_token: string; user_id: number; application_id: number; expires_in: number }
```
### PublicSessionRequest / Response
```ts
// Request
{
scenario_uuid: string;
input_mode?: string;
output_mode?: string;
language?: string;
resume_conversation_id?: string;
metadata?: Record;
}
// Response — Transport credentials (LiveKit today, see /introduction/transport)
{ livekit_token: string; livekit_url: string; expires_in: number }
```
## RestoreState
```ts
interface RestoreState {
checkpoint?: string;
data?: Record;
version?: number;
updated_at?: string;
}
```
---
# RPC contracts
Source: https://docs.scenaro.io/voice-agent/protocol/rpc/
Frontend tool execution uses [Transport](/introduction/transport) RPC (LiveKit RPC today).
Method names and timeouts are defined in `@scenaro/sdk/protocol`.
```ts
import {
RPC_METHODS,
RPC_TIMEOUTS,
resolveRpcMethodName,
} from '@scenaro/sdk/protocol';
```
## Built-in RPC methods
| Constant | Method name | Timeout |
|----------|-------------|---------|
| `UI_ACTIVITY_CHECK` | `scenaro__ui_activity_check` | 3000ms |
| `COMPOSER_START_DICTATION` | `composer_start_dictation` | 10000ms |
| `COMPOSER_END_DICTATION` | `composer_end_dictation` | 15000ms |
| `COMPOSER_CANCEL_DICTATION` | `composer_cancel_dictation` | 8000ms |
## Timeout constants
```ts
export const RPC_TIMEOUTS = {
AGENT_DEFAULT_MS: 8000,
FRONTEND_TOOL_MS: 10000,
UI_ACTIVITY_MS: 3000,
COMPOSER_START_MS: 10000,
COMPOSER_END_MS: 15000,
COMPOSER_CANCEL_MS: 8000,
} as const;
```
Frontend tool handlers must respond within `FRONTEND_TOOL_MS` (10s) or the agent receives a timeout error.
## Tool method resolution
Cockpit can override the RPC method name per tool:
```ts
resolveRpcMethodName({
name: 'search_collection_items',
frontend_method: 'custom_search_rpc', // optional override
});
// → 'custom_search_rpc' or 'search_collection_items'
```
The SDK registers RPC methods dynamically from the `config` payload — via session tools
(`useScenaroSession({ tools })`) or `FeatureProvider` / local feature runtime.
## Request / response shapes
### RpcToolRequest (inbound)
```ts
interface RpcToolRequest {
requestId: string;
tool?: string;
args?: Record;
sentAt?: number;
}
```
### RpcToolResponse (outbound)
```ts
interface RpcToolResponse {
requestId?: string;
code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED' | string;
data?: unknown;
}
```
## Example handler
```ts
async function searchHandler(requestId, args, settings, collections) {
const items = await searchAPI(args.query, settings);
return {
requestId,
response: { code: 'SUCCESS', data: { items: itemsForLLM } },
componentData: { items: itemsForUI, searchQuery: args.query },
};
}
```
The `response.data` is sent back to the agent. `componentData` updates the feature UI via `FeatureProvider`.
## Agent-side RPC
The voice agent may also call built-in methods like `scenaro__ui_activity_check` to verify the user is interacting with feature panels. The SDK's `ui-activity-tracker` responds automatically during an active session.
---
# Embed voice on your website
Source: https://docs.scenaro.io/guides/recipes/embed-voice-website/
This recipe walks through a complete production embed — from install to a working voice button on your site.
## Goal
A visitor clicks "Talk to us", gets a live voice session with your Scenaro agent, and sees feature panels (search, product details) alongside the conversation.
## Project structure
```
src/
├── lib/
│ └── scenaro/
│ ├── client.ts # createScenaroClient
│ ├── providers.tsx # ScenaroProvider + FeatureProvider
│ └── features/
│ ├── registry.ts # FEATURE_REGISTRY map
│ └── search/
│ ├── component.tsx
│ └── handler.ts
├── components/
│ └── VoiceWidget.tsx
└── App.tsx
```
## Step 1 — Client
```ts
// src/lib/scenaro/client.ts
import { createScenaroClient } from '@scenaro/sdk/client';
export const scenaroClient = createScenaroClient({
apiUrl: import.meta.env.VITE_SCENARO_API_URL,
apiPathPrefix: '/v1',
});
```
## Step 2 — Providers
```tsx
// src/lib/scenaro/providers.tsx
import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react';
import { scenaroClient } from './client';
import { FEATURE_REGISTRY } from './features/registry';
export function ScenaroProviders({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Step 3 — Voice widget
```tsx
// src/components/VoiceWidget.tsx
'use client';
import { useScenaroSession } from '@scenaro/sdk/react';
const PUBLICATION_UUID = import.meta.env.VITE_SCENARO_PUBLICATION_UUID;
export function VoiceWidget() {
const { status, error, start, end } = useScenaroSession({
scenario: PUBLICATION_UUID,
});
return (
{error &&
{error.message}
}
{status !== 'connected' ? (
) : (
)}
);
}
```
No `RoomContext` or listener component to wire up — the hook handles the data channel, config handshake, and tool RPC internally. Omit `language` to use the scenario's configured language, or pass one (e.g. `language: 'en-US'`) to override it.
## Step 4 — Mount in App
```tsx
import { ScenaroProviders } from './lib/scenaro/providers';
import { VoiceWidget } from './components/VoiceWidget';
export default function App() {
return (
{/* your site */}
);
}
```
## Environment variables
```env
VITE_SCENARO_API_URL=https://api.scenaro.io
VITE_SCENARO_PUBLICATION_UUID=your-publication-uuid
```
## Production checklist
- [ ] HTTPS required for microphone access
- [ ] Publication UUID points to production scenario
- [ ] Feature registry covers all Cockpit-configured tools
- [ ] Error states shown to user (mic denied, network failure)
- [ ] Session end cleans up UI state
## Variations
- **Floating button** — position widget fixed bottom-right
- **Mute button** — `setMicrophoneEnabled(false)` from the hook + custom mic toggle
- **Text input** — `sendText('...')` alongside (or instead of) voice
## See also
- [E-commerce embed](/guides/recipes/ecommerce-embed)
- [Production checklist](/guides/production-checklist)
---
# Custom feature
Source: https://docs.scenaro.io/guides/recipes/custom-feature/
Add a custom feature that the voice agent can trigger via tool calls, with a React UI panel.
## Scenario
You want a "store locator" feature: the agent calls `find_nearby_stores`, your frontend queries an API, shows a map, and returns results to the agent.
## Step 1 — Define the handler
```ts
// src/lib/scenaro/features/store-locator/handler.ts
import type { ToolHandler } from '@scenaro/sdk/react';
export const findNearbyStoresHandler: ToolHandler = async (
requestId,
args: { city?: string; radius_km?: number },
) => {
const stores = await fetchStores(args.city, args.radius_km ?? 10);
return {
requestId,
response: {
code: 'SUCCESS',
data: {
stores: stores.map((s) => ({
name: s.name,
address: s.address,
distance_km: s.distance,
})),
},
},
componentData: {
stores,
city: args.city,
},
};
};
```
## Step 2 — Build the component
```tsx
// src/lib/scenaro/features/store-locator/component.tsx
interface Props {
settings: Record;
toolState: Record;
}
export function StoreLocatorFeature({ toolState }: Props) {
const stores = (toolState.stores as Store[]) ?? [];
if (!stores.length) return null;
return (
Nearby stores
{stores.map((store) => (
{store.name} — {store.distance_km} km
))}
);
}
```
## Step 3 — Register in the map
```ts
// src/lib/scenaro/features/registry.ts
import { StoreLocatorFeature } from './store-locator/component';
import { findNearbyStoresHandler } from './store-locator/handler';
export const FEATURE_REGISTRY = new Map([
['store:locator', {
component: StoreLocatorFeature,
handlers: { find_nearby_stores: findNearbyStoresHandler },
primaryTool: 'find_nearby_stores',
toolNames: ['find_nearby_stores'],
}],
]);
```
## Step 4 — Configure in Cockpit
In Cockpit, add a feature with:
- **Type**: `store:locator` (must match registry key)
- **Tool name**: `find_nearby_stores`
- **frontend_method**: optional RPC override
## Step 5 — Test
1. Start a voice session
2. Ask the agent to find nearby stores
3. Verify RPC fires, UI renders, agent receives `SUCCESS` data
## Error handling
```ts
return {
requestId,
response: { code: 'ERROR', data: { stores: [], error: 'City not found' } },
componentData: { stores: [], city: args.city },
};
```
Return `ERROR` (not throw) so the agent can explain the failure to the user.
## Tips
- Keep `response.data` concise — it's sent to the LLM
- Put rich UI data in `componentData` — it's for your React component only
- Match feature `type` in Cockpit to registry key exactly
- No UI panel needed? Skip the registry and pass the handler directly via the `tools` option of `useScenaroSession` — it receives `(args, context)` and returns the result, no request IDs or response wrapping
---
# Product search feature
Source: https://docs.scenaro.io/guides/recipes/product-search/
This recipe implements `collection:items:search`, the most common feature across Scenaro e-commerce experiences (Lupi, Le Petit Ballon, Urbansider, etc.).
## Feature overview
| Property | Value |
|----------|-------|
| Feature type | `collection:items:search` |
| Primary tool | `search_collection_items` |
| UI | Search results panel with product rows |
| Data source | Platform API collections |
## Handler pattern
```ts
export const searchCollectionItemsHandler = async (
requestId: string,
args: { query?: string; filter?: string },
settings: Record,
collections: unknown[],
) => {
const collectionId = getEffectiveCollectionId(settings, collections);
const limit = Number(settings.limit ?? 10);
const rawItems = await collectionItemsAPI.search({
collectionId,
query: args.query ?? '',
filter: args.filter ?? '',
limit,
searchField: settings.search_field as string,
searchMode: settings.search_mode as string,
});
const llmItems = rawItems.map((item) =>
filterItemByReturnFields(item, settings.return_fields as string[]),
);
return {
requestId,
response: { code: 'SUCCESS', data: { items: llmItems } },
componentData: {
items: rawItems,
searchQuery: args.query,
searchField: settings.search_field,
limit,
},
};
};
```
## Registry entry
```ts
['collection:items:search', {
component: SearchFeature,
handlers: { search_collection_items: searchCollectionItemsHandler },
primaryTool: 'search_collection_items',
toolNames: ['search_collection_items'],
propsSelector: (toolState, settings, collections) => ({
items: toolState.items,
searchQuery: toolState.searchQuery,
settings,
collections,
}),
}],
```
Rendering results in your own UI instead of a feature panel? Skip the registry and pass the
handler via the `tools` option of `useScenaroSession` — it receives `(args, context)` where
`context.settings` and `context.collections` come from the scenario config.
## Search component essentials
Your `SearchFeature` component should:
1. Read `toolState.items` from `useFeatures()`
2. Render product rows using a template (e.g. `ProductTemplate`)
3. Handle empty results with a friendly message
4. Support scroll boundary prevention on mobile
```tsx
import { useFeatures } from '@scenaro/sdk/react';
export function SearchFeature({ toolState, settings, collections }) {
const items = toolState.items ?? [];
const template = getRowTemplate(settings.template);
return (
{items.map((item) => template.render(item))}
);
}
```
## Authenticated API calls
Search API calls need the user JWT. Use the shared client:
```ts
import { scenaroClient } from '../client';
const token = await scenaroClient.getValidUserToken();
// Pass token to your collection API client
```
## Agent prompt hints
```
When the user asks to find products:
1. Call search_collection_items with a clear query
2. Say "I'm showing you the results" — don't read every item
3. Offer to show details on a specific item
```
## Related features
| Feature | Tool | Purpose |
|---------|------|---------|
| `collection:items:display` | `display_collection_item` | Product detail sheet |
| `collection:items:comparison` | `compare_collection_items` | Side-by-side compare |
| `bucket:list:crud` | `add_to_bucket`, etc. | Shopping list |
## Reference implementation
See `experiences/lupi/src/lib/scenaro-sdk/features/collection-items-search/` in the monorepo for a full production implementation.
---
# E-commerce embed
Source: https://docs.scenaro.io/guides/recipes/ecommerce-embed/
Build a voice shopping assistant with catalog search, product details, comparison, and basket management.
## Feature set
Register all four standard e-commerce features:
```ts
export const FEATURE_REGISTRY = new Map([
['collection:items:search', {
component: SearchFeature,
handlers: { search_collection_items: searchHandler },
primaryTool: 'search_collection_items',
toolNames: ['search_collection_items'],
}],
['collection:items:display', {
component: DisplayFeature,
handlers: { display_collection_item: displayHandler },
primaryTool: 'display_collection_item',
toolNames: ['display_collection_item'],
}],
['collection:items:comparison', {
component: ComparisonFeature,
handlers: { compare_collection_items: compareHandler },
primaryTool: 'compare_collection_items',
toolNames: ['compare_collection_items'],
}],
['bucket:list:crud', {
component: BucketFeature,
handlers: {
add_to_bucket: addHandler,
remove_from_bucket: removeHandler,
list_bucket: listHandler,
clear_bucket: clearHandler,
},
primaryTool: 'add_to_bucket',
toolNames: ['add_to_bucket', 'remove_from_bucket', 'list_bucket', 'clear_bucket'],
}],
]);
```
## Layout pattern
```
┌─────────────────────────────────────────┐
│ Your e-commerce site (header, nav) │
├──────────────────────┬──────────────────┤
│ Main content │ Feature panel │
│ │ (search/cart) │
│ │ │
├──────────────────────┴──────────────────┤
│ Voice controls (mic, end session) │
└─────────────────────────────────────────┘
```
Use `FeatureProvider` with `initialPosition="right"` and control visibility via `setVisibility`.
## Collection API client
Create a thin API layer that uses the Scenaro JWT:
```ts
async function apiFetch(path: string, options = {}) {
const token = await scenaroClient.getValidUserToken();
return fetch(`${API_URL}/v1${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options.headers,
},
});
}
```
## Product templates
Use a shared `ProductTemplate` for consistent row rendering across search and comparison:
```ts
// templates/ProductTemplate.ts
export function getRowTemplate(templateName: string) {
switch (templateName) {
case 'wine': return WineRowTemplate;
case 'product': return DefaultProductTemplate;
default: return DefaultProductTemplate;
}
}
```
## Session item map
Track which products the user has viewed in the current session (used by display and comparison):
```ts
// shared/session-item-map.ts
export const sessionItemMap = new Map();
```
## End-of-conversation workflow
Register `workflow:conversation:end` to handle checkout redirects:
```ts
['workflow:conversation:end', {
component: EndConversationFeature,
handlers: { end_conversation: endHandler },
primaryTool: 'end_conversation',
}],
```
## Cockpit configuration
1. Connect your product collection in Cockpit
2. Enable all four features with appropriate tool settings
3. Set `return_fields` on search to limit LLM context size
4. Configure the agent prompt for shopping flow
## Production experiences
Reference implementations in the monorepo:
| Experience | Features |
|------------|----------|
| `experiences/lupi` | Full e-commerce set |
| `experiences/lepetitballon` | Search + comparison + bucket |
| `experiences/urbansider` | Search + display + custom tool handlers |
## Next steps
- [Product search](/guides/recipes/product-search) — deep dive on search handler
- [Production checklist](/guides/production-checklist)
---
# Multi-language sessions
Source: https://docs.scenaro.io/guides/recipes/multi-language/
Run Scenaro voice sessions in multiple languages by aligning Cockpit configuration, frontend language tags, and UI translations.
## Frontend language tag
Pass a BCP-47 language tag to `useScenaroSession`:
```tsx
// French session
useScenaroSession({ scenario: SCENARIO_UUID, language: 'fr-FR' });
// English session
useScenaroSession({ scenario: SCENARIO_UUID, language: 'en-US' });
```
This sets `language` on `POST /v1/public/session` and configures STT/TTS for the session.
`language` is optional. If you omit it, the session uses the language configured on the
scenario in Cockpit — the SDK no longer defaults to `fr-FR`. Only pass a tag when the
user explicitly picks a language.
## Cockpit setup
For each language:
1. Create or duplicate the scenario with a localized system prompt
2. Publish a separate publication UUID per language (or use dynamic language if supported)
3. Configure voice stack voices appropriate for the target language
## Frontend i18n
Feature components should use your app's i18n system:
```tsx
import { useTranslation } from '../hooks/useTranslation';
export function SearchFeature({ toolState }) {
const { t } = useTranslation();
const items = toolState.items ?? [];
return (
{t('search.results_title')}
{items.length === 0 &&
{t('search.no_results')}
}
{items.map((item) => )}
);
}
```
## Language selector pattern
```tsx
const LANGUAGES = [
{ tag: 'fr-FR', label: 'Français' },
{ tag: 'en-US', label: 'English' },
{ tag: 'de-DE', label: 'Deutsch' },
];
function LanguageSelector({ onSelect }) {
return (
);
}
// In your widget — undefined means "use the scenario's language":
const [language, setLanguage] = useState();
const { start } = useScenaroSession({ scenario: SCENARIO_UUID, language });
```
## Prompt guidelines per language
| Rule | Reason |
|------|--------|
| Write the system prompt in the target language | LLM responds in the prompt language |
| Keep tool names in English | Tool names are code identifiers, not spoken |
| Localize feature `name` fields in Cockpit | Agent uses feature names in conversation |
| Test STT with native accents | Recognition quality varies by provider and locale |
## Text input for non-voice contexts
For accessibility or noisy environments, disable the microphone and send text instead:
```tsx
const { start, sendText, setMicrophoneEnabled } = useScenaroSession({
scenario: SCENARIO_UUID,
language: 'fr-FR',
});
await start();
setMicrophoneEnabled(false);
sendText('Bonjour !');
```
## Experience reference
`experiences/urbansider` supports multiple languages with `useTranslation` hooks and per-locale Cockpit scenarios.
---
# Advanced: Transport extensions
Source: https://docs.scenaro.io/guides/advanced-transport-integration/
`@scenaro/sdk` separates a **portable session contract** from the **Transport** that carries
it — see [Transport](/introduction/transport) for the full picture. For the majority of
integrations — a voice button embedded on a page — the [quickstart](/introduction/quickstart)
is all you need, and you'll never see an engine-specific type. This guide is for the minority
of integrations that need more: custom audio visualizers, avatars, multi-participant layouts,
or anything else the current engine's ecosystem already does well.
This is an assumed extension point, not an escape hatch you're forced into. Scenaro doesn't
wrap or re-export the engine's components — you import them directly from their own packages
and use them next to `useScenaroSession`.
## 1. When to use this guide
Reach for Transport extensions when you need:
- Custom audio visualizers or waveforms
- Agent avatars driven by speaking state
- Multi-participant layouts
- Anything else already solved by the current engine's component library
If you just need `status`, `messages`, `config`, or tool calls, stay on the portable contract —
you don't need anything in this guide.
## 2. Portable contract vs engine extension
`useScenaroSession` always returns the same portable contract (`status`, `agentState`,
`messages`, `config`, `tools`, events) regardless of which Transport implementation is running
underneath. On top of that, it also exposes `room` — an **engine extension**: the live engine
handle for the current Transport implementation.
| Contract | Stability | Where it's documented |
|---|---|---|
| `status`, `agentState`, `messages`, `config`, `tools`, events | Portable — stable across future Transport implementations | [Quickstart](/introduction/quickstart), [SDK reference](/sdk-reference/react) |
| `room`, `RoomContext`, `@livekit/components-react` hooks/components | Engine-specific (LiveKit today) — not guaranteed to carry over to a future Transport implementation | This guide |
Code that reads `status` or `agentState` keeps working if Scenaro ever adds another Transport
implementation (e.g. Pipecat) behind `useScenaroSession`. Code that calls
`room.localParticipant...` or uses `useVoiceAssistant()` is written against the current engine
directly — the same trade-off you'd make using that engine standalone. See
[Transport](/introduction/transport) for the internal boundary that makes this possible.
## 3. LiveKit implementation (today)
The current Transport implementation is [LiveKit](https://livekit.io). This section is the
concrete, engine-specific how-to.
### Install the LiveKit peer dependencies
`livekit-client` and `@livekit/components-react` are peer dependencies of `@scenaro/sdk` —
install them once you start using this guide (the [quickstart](/introduction/quickstart) does
not require them):
```bash npm
npm install livekit-client @livekit/components-react
```
```bash pnpm
pnpm add livekit-client @livekit/components-react
```
### Using `room` with LiveKit components
`ScenaroProvider` injects `RoomContext` for you, so LiveKit's components and hooks work as soon
as a session is active — no manual `` wiring required.
```tsx
import { useScenaroSession } from '@scenaro/sdk/react';
import { useVoiceAssistant, BarVisualizer, RoomAudioRenderer } from '@livekit/components-react';
function AssistantAvatar() {
const { status, room } = useScenaroSession({ scenario: SCENARIO_UUID }); // portable contract
const { state, audioTrack } = useVoiceAssistant(); // LiveKit, direct
if (status !== 'connected' || !room) return null;
return (
<>
>
);
}
```
Scenaro never re-exports these components. When LiveKit ships something new — Agents UI, a new
Session API, a new visualizer — you use it immediately, without waiting for a Scenaro
equivalent.
### What stays yours to maintain
Because this code is written against LiveKit's API surface directly, keep in mind:
- **A future engine swap isn't automatic here.** If Scenaro adds a Pipecat Transport
implementation behind `useScenaroSession` in the future, the portable contract will keep
working unchanged. Code that calls `room.*` or LiveKit component hooks will need to be ported
by hand — the same way any code written against a specific vendor's low-level API would.
- **Support boundary.** Scenaro's contract covers session, auth, config, tools, and features.
Anything under `room` — audio devices, track subscriptions, participant behavior — is
LiveKit's surface; consult the [LiveKit docs](https://docs.livekit.io) for it.
- **Don't mix contracts in shared components.** Keep components that only need the portable
contract free of LiveKit imports, so they stay reusable if the Transport implementation ever
changes.
## Next steps
- [Transport](/introduction/transport) — the engine-agnostic boundary behind this guide
- [SDK reference — React](/sdk-reference/react) — full `useScenaroSession` and `ScenaroSession` API
- [LiveKit Components React docs](https://docs.livekit.io/reference/components/react/) — hooks and components available on `room`
---
# Production checklist
Source: https://docs.scenaro.io/guides/production-checklist/
Use this checklist before shipping a Scenaro integration to production.
## Environment
| Variable | Required | Example |
|----------|----------|---------|
| API URL | Yes | `https://api.scenaro.io` |
| Publication UUID | Yes | Cockpit production publication |
| `apiPathPrefix` | Check | `'/v1'` or `''` if URL includes `/v1` |
Double-check `apiPathPrefix`. A common bug is `/v1/v1/public/token` when the base URL already includes `/v1`.
## Security
- [ ] HTTPS on your site (required for microphone)
- [ ] Publication UUID is the production publication, not staging
- [ ] No admin API keys in frontend code
- [ ] `user_token` JWT is stored in localStorage only (never logged to analytics)
## Dependencies
- [ ] `@scenaro/sdk` version pinned in `package.json`
- [ ] `react`, `react-dom` installed
- [ ] If using [Transport extensions](/guides/advanced-transport-integration): `livekit-client`, `@livekit/components-react` `>=2.0.0` installed
## Feature registry
- [ ] Every tool in Cockpit config has a matching registry entry
- [ ] Handlers return within 10s (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`)
- [ ] Error responses use `code: 'ERROR'` with agent-friendly data
## Session lifecycle
- [ ] `end()` called on page unload or navigation (optional `beforeunload` handler)
- [ ] `onSessionEnd` resets feature UI state
- [ ] `status === 'reconnecting'` surfaced to the user (brief indicator)
- [ ] `ScenaroError` codes handled: `MIC_PERMISSION_DENIED`, `CONNECTION_LOST`, `AGENT_UNAVAILABLE`
## Performance
- [ ] `ScenaroProvider` mounted once at the app root — the identity JWT is cached and reused across sessions
- [ ] Feature components lazy-loaded if heavy
- [ ] Search results limited via Cockpit `limit` setting
## Browser support
- [ ] Tested Chrome, Safari, Firefox (desktop + mobile)
- [ ] Microphone permission flow handled gracefully
- [ ] Fallback message when WebRTC blocked
## Monitoring
- [ ] Client-side error logging for connection failures
- [ ] Track session start/end events in your analytics
- [ ] Monitor API error rates on `/public/token` and `/public/session`
## Build
```bash
# Verify SDK builds in your CI
pnpm --filter @scenaro/sdk build
pnpm --filter @scenaro/sdk test
```
For Next.js:
```js
transpilePackages: ['@scenaro/sdk'],
```
## Deployment
- [ ] Mintlify docs URL in your README: `https://docs.scenaro.io`
- [ ] Changelog reviewed for breaking changes: [Changelog](/changelog)
---
# Migration from vendored SDK
Source: https://docs.scenaro.io/guides/migration-from-vendored-sdk/
There are two migration paths, depending on where your project is today:
1. **Vendored `src/lib/scenaro-sdk/` folder** → published `@scenaro/sdk` package
2. **Pre-0.3 API** (`useScenarioSession` + `AgentListener`) → `useScenaroSession`
Both end at the same place: one provider, one hook, no transport wiring.
## Before and after
| Vendored | @scenaro/sdk 0.1–0.2 | @scenaro/sdk 0.3 |
|----------|----------------------|------------------|
| `ScenarioPlayer` monolith | `useScenarioSession` + `AgentListener` + `RoomContext` | `useScenaroSession` (all-in-one) |
| `generateToken()` one-shot | `identifyUser()` + `startSession()` | Handled by `start()` |
| `window.__` event bus | `useScenaroEvents()` scoped bus | `useScenaroEvents()` (unchanged) |
| Copied protocol files | `@scenaro/sdk/protocol` | `@scenaro/sdk/protocol` (unchanged) |
| 8 divergent copies | One npm package | One npm package |
## Migration steps
```bash
pnpm add @scenaro/sdk livekit-client @livekit/components-react
```
```ts
// Before (vendored)
import { scenariosAPI } from './lib/scenaro-sdk/api/scenarios';
// After
import { createScenaroClient } from '@scenaro/sdk/client';
export const scenaroClient = createScenaroClient({
apiUrl: import.meta.env.VITE_SCENARO_API_URL,
apiPathPrefix: '/v1',
});
```
```tsx
// Before (vendored)
import { SdkSessionProvider } from './lib/scenaro-sdk/SdkSessionProvider';
// After
import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react';
```
```tsx
// Before (vendored)
import useScenarioSession from './lib/scenaro-sdk/hooks/useScenarioSession';
// Before (0.1–0.2 — removed in 0.3.0)
import { useScenarioSession, AgentListener } from '@scenaro/sdk/react';
// After (0.3)
import { useScenaroSession } from '@scenaro/sdk/react';
const { status, start, end } = useScenaroSession({ scenario: PUBLICATION_UUID });
```
`useScenaroSession` handles the data channel, config handshake, tool RPC, and `RoomContext`
injection internally. Delete these from your components:
```tsx
// Before (0.1) — all of this goes away
{isConnected && }
{/* ... */}
```
Remove any `generateToken` usage. The hook runs identify + session start automatically
on `start()`.
```tsx
// Before (vendored)
window.__.on('config', handler);
// After
const events = useScenaroEvents();
events.on('config', handler);
```
Delete duplicated `AgentListener`, `protocol/`, and `auth/` files. Keep only feature
components and your registry.
## Pre-0.3 API mapping (historical)
These exports were **removed in 0.3.0**. If you still import them, upgrade to 0.3.
| Removed (`useScenarioSession`) | Replacement (`useScenaroSession`) |
|--------------------------------|-----------------------------------|
| `connect({ language })` | `start()` — pass `language` as a hook option |
| `disconnect()` | `end()` |
| `isConnected` / `isConnecting` | `status` (`'connected'`, `'connecting'`, …) |
| `error` (string) | `error` (`ScenaroError` with `code`) |
| `` | Built in — remove it |
| `RoomContext.Provider` | Built in — remove it |
| `useConversationMessages()` | `messages` from the hook |
| `room` | `room` (advanced only — see [Transport extensions](/guides/advanced-transport-integration)) |
| `runtimeConfig` | `rawConfig` from the hook |
## What to keep locally
| Keep in your app | Provided by @scenaro/sdk |
|------------------|--------------------------|
| Feature components | `ScenaroProvider` |
| Feature registry / local FeatureManager | `useScenaroSession` |
| Brand CSS | Data channel + session tool RPC |
| Collection API clients | Protocol types |
| Product templates | Session storage |
## Storage migration
Since `@scenaro/sdk@0.1.1`, legacy storage keys are migrated automatically on read. No manual
migration needed for users with existing sessions.
## Verify
```bash
pnpm build
# Test: start() → status 'connected' → config received → tool call → end()
```
## Need help?
- [Quickstart](/introduction/quickstart)
- [Official docs](https://docs.scenaro.io)
- [GitHub issues](https://github.com/scenaro/projects/issues)
---
# @scenaro/sdk/react
Source: https://docs.scenaro.io/sdk-reference/react/
React bindings for Scenaro voice sessions. Peer dependencies: `react`, `react-dom`,
`livekit-client`, `@livekit/components-react` — the latter two back the current
[Transport](/introduction/transport) implementation and are only required at runtime if you use
[Transport extensions](/guides/advanced-transport-integration).
```tsx
import {
ScenaroProvider,
useScenaroSession,
FeatureProvider,
useFeatures,
} from '@scenaro/sdk/react';
```
## ScenaroProvider
Owns the shared [`ScenaroSession`](/sdk-reference/session) and the API client. Also injects
LiveKit's `RoomContext` (the current [Transport](/introduction/transport) implementation) so
`@livekit/components-react` hooks work without extra wiring — see
[Advanced: Transport extensions](/guides/advanced-transport-integration).
```tsx
{children}
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `apiUrl` | `string` | — | API base URL (or pass `client`) |
| `client` | `ScenaroClient` | — | Pre-built client instance |
| `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints |
| `storagePrefix` | `string` | `''` | Prefix for localStorage keys |
| `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter |
Throws if neither `client` nor `apiUrl` is provided. The session's transport listeners are
released automatically when the provider unmounts.
## useScenaroSession
Single entry point for the session contract. Must be called under a `ScenaroProvider`.
```tsx
const {
status, agentState, messages, config, conversationId, error,
start, end, sendText, setMicrophoneEnabled,
room,
} = useScenaroSession({
scenario: 'publication-uuid',
tools: {
search_products: async ({ query }) => searchCatalog(query),
},
onMessage: (message) => {},
onError: (error) => {},
onSessionEnd: (info) => {},
});
```
### Options
`UseScenaroSessionOptions` — everything from [`StartSessionOptions`](/sdk-reference/session#startsessionoptions)
plus React callbacks:
| Option | Type | Description |
|--------|------|-------------|
| `scenario` | `string` | Publication UUID (required) |
| `language` | `string` | BCP-47 — omit to use the scenario's configured language |
| `inputMode` / `outputMode` | `'audio' \| 'text'` | Default `'audio'` |
| `microphone` | `boolean` | Enable mic on connect (default `true`) |
| `externalId` | `string \| null` | Stable visitor identifier |
| `resumeConversationId` | `string` | Resume an earlier conversation |
| `metadata` | `Record` | Attached to the session |
| `tools` | `Record` | Frontend tool handlers |
| `onMessage` | `(message: ConversationMessage) => void` | New conversation message |
| `onError` | `(error: ScenaroError) => void` | Typed error callback |
| `onSessionEnd` | `(info: SessionEndInfo) => void` | Session ended (any reason) |
### Return value
| Field | Type | Contract |
|-------|------|----------|
| `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` | Portable |
| `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` | Portable |
| `messages` | `ConversationMessage[]` | Portable |
| `config` | `SessionConfig \| null` | Portable |
| `conversationId` | `string \| null` | Portable |
| `error` | `ScenaroError \| null` | Portable |
| `start` | `(overrides?) => Promise` | Portable — overrides can't change `scenario` |
| `end` | `() => Promise` | Portable |
| `sendText` | `(text: string) => Promise` | Portable |
| `setMicrophoneEnabled` | `(enabled: boolean) => Promise` | Portable |
| `room` | `Room \| null` | **Engine extension** (LiveKit today) — see [Transport extensions](/guides/advanced-transport-integration) |
| `rawConfig` | `ConfigPayload \| null` | Wire-format config (`restore_state`, tool wiring) |
### Tool handlers
Plain functions. Return value is serialized back to the agent; throwing produces an `ERROR`
response; exceeding the RPC timeout produces `TIMEOUT` and aborts `context.signal`.
```ts
type ToolHandler = (
args: TArgs,
context: {
toolName: string;
settings: Record; // from the scenario config
collections: Collection[]; // typed — see @scenaro/sdk/protocol
signal: AbortSignal; // aborted on timeout
},
) => TResult | Promise;
```
## FeatureProvider
Wires tool calls to UI components via an injectable registry. See
[Feature registry](/build/feature-registry).
| Prop | Type | Description |
|------|------|-------------|
| `registry` | `Map` | Feature registry |
| `onResetBuckets` | `() => void` | Called on session reset |
| `clearHistoryOnSessionEnd` | `boolean` | Clear feature history on end |
| `initialVisibility` | `boolean` | Panel visible by default |
| `initialPosition` | `'left' \| 'right' \| 'mobile' \| 'hidden'` | Panel position |
### useFeatures
Returns `FeatureRuntimeContextValue`:
| Method | Description |
|--------|-------------|
| `executeTool(name, requestId, args)` | Run a tool handler |
| `selectFeature(name)` | Activate a feature panel |
| `setVisibility(visible, position)` | Show/hide panel |
| `getFeatureComponent(name)` | Get React component |
| `getToolMapping(toolName)` | Resolve tool → feature |
| `navigateHistory(direction)` | History back/forward |
| `reset()` | Reset all state |
## Context hooks
| Hook | Returns |
|------|---------|
| `useScenaro()` | `{ client, events, session }` |
| `useScenaroClient()` | `ScenaroClient` |
| `useScenaroEvents()` | `ScenaroEventBus` |
---
# ScenaroSession
Source: https://docs.scenaro.io/sdk-reference/session/
`ScenaroSession` is the framework-agnostic core of the SDK. `useScenaroSession` is a thin
React binding over it — if you're not using React (vanilla JS, Vue, Svelte…), use this class
directly.
```ts
import { ScenaroSession } from '@scenaro/sdk';
const session = new ScenaroSession({ apiUrl: 'https://api.scenaro.io' });
session.on('status-change', (status) => console.log(status));
session.on('message', (message) => render(message));
await session.start({
scenario: 'publication-uuid',
tools: {
search_products: async ({ query }) => searchCatalog(query),
},
});
```
## Constructor
```ts
new ScenaroSession(options: ScenaroSessionOptions)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiUrl` | `string` | — | API base URL (or pass `client`) |
| `client` | `ScenaroClient` | — | Pre-built client instance |
| `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints |
| `storagePrefix` | `string` | `''` | Prefix for localStorage keys |
| `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter |
| `transport` | `Transport` | `LiveKitTransport` | Internal, non-public interface — injectable for tests |
Throws if neither `client` nor `apiUrl` is provided.
## State
All state is readable synchronously; changes are announced through [events](#events).
| Getter | Type |
|--------|------|
| `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` |
| `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` |
| `messages` | `ConversationMessage[]` |
| `config` | `SessionConfig \| null` |
| `rawConfig` | `ConfigPayload \| null` — wire format (`restore_state`, tool wiring) |
| `conversationId` | `string \| null` |
| `room` | `unknown` — **engine extension** (LiveKit today), see [Transport extensions](/guides/advanced-transport-integration) |
## Methods
### start
```ts
start(options: StartSessionOptions): Promise
```
Identifies the visitor, starts a session, registers tools, and connects Transport.
Throws `ScenaroError` with code `IDENTIFY_FAILED` or `SESSION_START_FAILED`.
#### StartSessionOptions
| Field | Type | Default |
|-------|------|---------|
| `scenario` | `string` | required |
| `language` | `string` | scenario's configured language |
| `inputMode` / `outputMode` | `'audio' \| 'text'` | `'audio'` |
| `microphone` | `boolean` | `true` |
| `externalId` | `string \| null` | auto-generated |
| `resumeConversationId` | `string` | — |
| `metadata` | `Record` | — |
| `tools` | `Record` | `{}` |
### end
```ts
end(): Promise
```
Unregisters tools, disconnects Transport, and sets `status` to `'ended'`. The session
can be started again with `start()`.
### sendText / setMicrophoneEnabled
```ts
sendText(text: string): Promise
setMicrophoneEnabled(enabled: boolean): Promise
```
In-session actions never throw — failures are emitted on the `error` event instead, so
fire-and-forget call sites don't produce unhandled rejections.
### destroy
```ts
destroy(): void
```
Releases Transport listeners and timers. Call when the owning scope is torn down
(`ScenaroProvider` does this automatically on unmount).
## Events
Kebab-case event names, subscribed with `on` / `off`:
| Event | Payload |
|-------|---------|
| `status-change` | `SessionStatus` |
| `agent-state` | `AgentState` |
| `message` | `ConversationMessage` |
| `config` | `SessionConfig` |
| `session-end` | `{ reason: string }` — `'shutdown'`, `'agent_disconnected'`, `'transport_disconnected'` |
| `error` | `ScenaroError` |
## Errors
```ts
class ScenaroError extends Error {
code: 'IDENTIFY_FAILED' | 'SESSION_START_FAILED' | 'CONNECTION_LOST'
| 'MIC_PERMISSION_DENIED' | 'TOOL_TIMEOUT' | 'AGENT_UNAVAILABLE';
cause?: unknown;
}
```
`start()` throws; everything after connection emits on `error` instead.
## SessionConfig
A curated view of the scenario config — backend plumbing (`company_id`, `voice_stack`,
timestamps, tool wiring) is deliberately not exposed.
```ts
interface SessionConfig {
features: SessionFeature[]; // { name, type, settings }
collections: Collection[]; // typed — see @scenaro/sdk/protocol
avatarEnabled: boolean;
audioDictationEnabled: boolean;
}
```
---
# @scenaro/sdk/client
Source: https://docs.scenaro.io/sdk-reference/client/
Framework-agnostic HTTP client for the Scenaro public API. No React or Transport dependencies.
```ts
import {
createScenaroClient,
ScenaroClient,
isTokenExpired,
getJWTExpiry,
getCompanyIdFromToken,
} from '@scenaro/sdk/client';
```
## createScenaroClient
```ts
function createScenaroClient(config: ScenaroClientConfig): ScenaroClient
```
### ScenaroClientConfig
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `apiUrl` | `string` | required | API base URL (no trailing slash) |
| `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints |
| `storagePrefix` | `string` | `''` | Prefix for localStorage keys |
| `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter |
## ScenaroClient methods
### identifyUser
```ts
identifyUser(
scenarioUUID: string,
externalId?: string | null,
): Promise
```
`POST /public/token` — returns JWT identity. Idempotent if valid token exists in storage.
### startSession
```ts
startSession(
scenarioUUID: string,
options?: {
inputMode?: string; // default 'audio'
outputMode?: string; // default 'audio'
language?: string; // omit to use the scenario's configured language
sessionOptions?: SessionOptions;
},
): Promise
```
`POST /public/session` — returns Transport credentials (LiveKit today: `livekit_token`,
`livekit_url` — see [Transport](/introduction/transport)). Requires stored JWT. When `language`
is omitted, the backend applies the language configured on the scenario.
### getValidUserToken
```ts
getValidUserToken(): Promise
```
Returns stored token if valid, otherwise silently re-identifies.
### getStoredUserToken
```ts
getStoredUserToken(
scenarioUUID?: string | null,
applicationId?: number | null,
): string | null
```
Synchronous read from storage.
### clearStoredSession
```ts
clearStoredSession(scenarioUUID?: string | null): void
```
Clears identity blob and scenario pointer.
### getSessionUserId / getSessionApplicationId
```ts
getSessionUserId(): number | null
getSessionApplicationId(): number | null
```
Read from stored identity blob.
## JWT utilities
```ts
isTokenExpired(token: string): boolean // 90s buffer
getJWTExpiry(token: string): number | null // Unix timestamp
getCompanyIdFromToken(token: string): number | null
```
## Storage exports
```ts
import {
buildStorageKeys,
persistIdentity,
type StorageAdapter,
type SessionStoreKeys,
} from '@scenaro/sdk/client';
```
See [Session storage](/build/session-storage) for key format details.
## Types
Re-exported from `@scenaro/sdk/protocol`:
- `IdentityResponse`
- `PublicTokenRequest` / `PublicTokenResponse`
- `PublicSessionRequest` / `PublicSessionResponse`
- `SessionOptions`
---
# @scenaro/sdk/protocol
Source: https://docs.scenaro.io/sdk-reference/protocol/
Zero-dependency protocol package. Shared contract between the Python voice agent and all frontends.
```ts
import {
DATA_TOPICS,
toolDataTopic,
RPC_METHODS,
RPC_TIMEOUTS,
resolveRpcMethodName,
type ConfigPayload,
type ConversationMessage,
} from '@scenaro/sdk/protocol';
```
## Topics
```ts
const DATA_TOPICS = {
CONFIG: 'config',
CONFIG_ACK: 'config_ack',
CONFIG_REQUEST: 'config_request',
MESSAGE_NEW: 'message:new',
SESSION_END: 'session:end',
DICTATION_TRANSCRIPT: 'dictation:transcript',
EVENT_FIRE: 'event:fire',
STATE_UPDATE: 'state:update',
SESSION_CONTROL: 'session:control',
ASSISTANT_INTERRUPT: 'assistant:interrupt',
UI_ACTIVITY: 'ui:activity',
CHAT: 'lk.chat',
} as const;
type DataTopic = (typeof DATA_TOPICS)[keyof typeof DATA_TOPICS];
```
```ts
function toolDataTopic(toolName: string): string
// Returns `tool:${toolName}`
```
See [Topics reference](/voice-agent/protocol/topics) for the full table.
## Message types
| Type | Used on topic |
|------|---------------|
| `ConfigPayload` | `config` |
| `ConfigAckPayload` | `config_ack` |
| `ConfigRequestPayload` | `config_request` |
| `ConversationMessage` | `message:new` |
| `SessionEndPayload` | `session:end` |
| `PublicTokenRequest` / `PublicTokenResponse` | HTTP `/public/token` |
| `PublicSessionRequest` / `PublicSessionResponse` | HTTP `/public/session` |
| `IdentityResponse` | Client identity |
| `SessionOptions` | Session options |
| `RestoreState` | Config restore |
| `FeatureDefinition` | Config features |
| `ToolConfig` | Config tools |
| `Collection` | Config collections |
See [Message types](/voice-agent/protocol/messages) for field details.
## Collection
Content collections attached to the scenario, compiled server-side into the published
version and delivered on the `config` topic:
```ts
interface Collection {
id: number;
name: string;
description: string;
company_id: number;
default_view_id?: number;
view?: {
template: string; // e.g. "product"
mapping: Record; // e.g. { title: "{{source.name}}" }
};
search_fields?: Array<{ name: string; index_type: string }>;
filter_fields?: Array<{
name: string;
filter_key: string;
field_type: string;
enum_options?: Array<{ value: string; label: string }>;
}>;
}
```
## RPC
```ts
const RPC_METHODS = {
UI_ACTIVITY_CHECK: 'scenaro__ui_activity_check',
COMPOSER_START_DICTATION: 'composer_start_dictation',
COMPOSER_END_DICTATION: 'composer_end_dictation',
COMPOSER_CANCEL_DICTATION: 'composer_cancel_dictation',
} as const;
const RPC_TIMEOUTS = {
AGENT_DEFAULT_MS: 8000,
FRONTEND_TOOL_MS: 10000,
UI_ACTIVITY_MS: 3000,
COMPOSER_START_MS: 10000,
COMPOSER_END_MS: 15000,
COMPOSER_CANCEL_MS: 8000,
} as const;
```
```ts
function resolveRpcMethodName(tool: {
name: string;
frontend_method?: string;
}): string
```
### RPC payload types
```ts
interface RpcToolRequest {
requestId: string;
tool?: string;
args?: Record;
sentAt?: number;
}
interface RpcToolResponse {
requestId?: string;
code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED' | string;
data?: unknown;
}
```
See [RPC contracts](/voice-agent/protocol/rpc).
## Multi-language spec
The protocol package is the source of truth for cross-language SDKs. Future `scenaro-protocol` extraction (Python, Go) will align on these same definitions.
Source: `packages/sdk/src/protocol/` in the [scenaro/projects](https://github.com/scenaro/projects) monorepo.