Packages
@akapulu/react@akapulu/react-ui@akapulu/server
Quickstart (React)
AkapuluProvider takes paths to your local API routes:
connectPath: your local connect endpointupdatesPath: your local updates endpoint
@akapulu/server.
Backend connect and updates routes
Your localconnect route should:
- call
akapulu.connectConversation(...)with the Akapulu Labs connect schemascenario_id: stringavatar_id: stringruntime_vars?: Record<string, string>stt_keywords?: string[]record_conversation?: boolean
- return the connect response fields required by the SDK:
room_urltokenconversation_session_id
updates route should:
- read
conversation_session_idfrom the query param - call
akapulu.pollConversationUpdates(conversationSessionId) - return the updates payload fields required by the SDK:
call_is_readycompletion_percentlatest_update_textconversation_session_id
updatesPath with conversation_session_id in the URL query string.
Install @akapulu/server in your backend and create two routes:
POST /api/akapulu/connectGET /api/akapulu/updates?conversation_session_id=...
AkapuluProvider to those local paths:
connectPath: "/api/akapulu/connect"updatesPath: "/api/akapulu/updates"
Browser vs server
-
In the browser:
AkapuluProvider(viaconfig.endpoints.connectPathandconfig.endpoints.updatesPath) requests your connect and updates backend routes. -
On your server: those routes use
@akapulu/server, read the Akapulu Labs API key from server-side environment variable, and call the Akapulu Labs HTTP API. That way your api key stays on the server and does not get exposed in the browser
AkapuluProvider expects your local connect and updates endpoints to return the JSON schema described under Backend connect and updates routes above.
Passing custom payload to your local connect route
If you want,AkapuluProvider can send a custom JSON payload to your local connectPath route via config.connectBody.
- If
connectBodyis provided, the SDK sends it as the POST JSON body to your local connect route. - If
connectBodyis omitted, the SDK sendsPOST /connectPathwith no body.
Styling AkapuluConversation
@akapulu/react-ui supports slot-level customization via:
classNamefor the rootclassesfor slot class namesstylesfor slot inline style overrides
Example 1: Slot key overrides (classes + styles)
Use slot keys when you want to target specific UI parts directly from React props.
Example 2: Default class overrides (global CSS)
Use built-in default classes when you want to apply theme-like global styles from CSS.Example 3: data-slot overrides (stable CSS selectors)
Use data-slot selectors when you want explicit, inspectable selectors in DevTools.
Slot map
Each slot has both a default class and adata-slot marker so users can inspect and target it in DevTools without guessing.
Layout
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
container | Outer wrapper for the whole conversation UI | akapulu-conversation | container |
title | Top heading text | akapulu-title | title |
connectedLayout | Main connected-state grid wrapper | akapulu-connected-layout | connected-layout |
startButton | Idle/error state start-call button | akapulu-start-button | start-button |
Loading
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
loadingContainer | Wrapper for loading state UI | akapulu-loading-container | loading-container |
loadingSpinner | Loading spinner element | akapulu-loading-spinner | loading-spinner |
loadingLabel | Loading headline text (e.g. Connecting…) | akapulu-loading-label | loading-label |
loadingProgressTrack | Progress bar background track | akapulu-loading-progress-track | loading-progress-track |
loadingProgressFill | Progress bar filled portion | akapulu-loading-progress-fill | loading-progress-fill |
loadingStatusText | Detailed status text under progress bar | akapulu-loading-status-text | loading-status-text |
Error modal
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
errorModalBackdrop | Full-screen modal overlay behind error card | akapulu-error-modal-backdrop | error-modal-backdrop |
errorModalCard | Error modal content card | akapulu-error-modal-card | error-modal-card |
Tool events
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
toolToast | Floating toast/card for tool activity | akapulu-tool-toast | tool-toast |
Video + controls
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
videoPane | Video column container | akapulu-video-pane | video-pane |
videoSurface | Primary remote/bot video surface | akapulu-video-surface | video-surface |
botStateBadge | Speaking/listening/idle badge on video | akapulu-bot-state-badge | bot-state-badge |
pip | Local picture-in-picture preview tile | akapulu-pip | pip |
waitingVideo | Placeholder shown before video is available | akapulu-waiting-video | waiting-video |
controlMic | In-call mic button (video mode) | akapulu-control-mic | control-mic |
controlCam | In-call camera button (video mode) | akapulu-control-cam | control-cam |
controlEnd | In-call hang-up button (video mode) | akapulu-control-end | control-end |
Transcript
| Slot key | What it targets | Default class | data-slot value |
|---|---|---|---|
transcriptPane | Transcript column container | akapulu-transcript-pane | transcript-pane |
transcriptContainer | Scrollable transcript box | akapulu-transcript-container | transcript-container |
transcriptHeader | Transcript header row | akapulu-transcript-header | transcript-header |
nodeChip | Current node chip in transcript header | akapulu-node-chip | node-chip |
transcriptRowUser | User transcript row/bubble | akapulu-transcript-row-user | transcript-row-user |
transcriptRowBot | Bot transcript row/bubble | akapulu-transcript-row-bot | transcript-row-bot |
Behavior customization (handlers + custom elements)
Beyond styles,@akapulu/react-ui lets you customize behavior and rendering for transcript/tool events directly on AkapuluConversation.
Built-in handler props on AkapuluConversation
transcriptFilter(entry)to hide transcript rowsrenderTranscriptEntry(entry)to render transcript rows with your own JSXonToolEvent(tool)to run side effects when a tool event arrivesrenderToolEvent(tool)to replace the default tool toast elementtoolEventTimeoutMsto control how long the tool toast stays visible
toolEventTimeoutMs behavior:
- default:
4000 - pass a number to customize the auto-hide timeout
- pass
nullto disable auto-hide
TranscriptEntry shape (entry):
| Field | Type | Description |
|---|---|---|
id | string | Stable transcript row id |
speaker | "user" | "bot" | Who produced this transcript chunk |
text | string | Transcript text |
timestamp | string | Event timestamp |
isFinal | boolean | Whether this transcript row is finalized |
NormalizedToolEvent shape (tool) by tool type:
RAG tool event
| Field | Type | Description |
|---|---|---|
messageType | "RAG" | RAG tool event |
functionName | string | RAG function/tool name |
summary | string | "RAG tool called" |
query | string | undefined | Query text |
vision tool event
| Field | Type | Description |
|---|---|---|
messageType | "vision" | Vision tool event |
functionName | string | Vision function/tool name |
summary | string | "Vision tool called" |
http tool event
| Field | Type | Description |
|---|---|---|
messageType | "http" | HTTP tool event |
functionName | string | HTTP function/tool name |
summary | string | "HTTP endpoint called" |
argsJson | string | undefined | Pretty JSON string of body |
body | Record<string, unknown> | Outgoing HTTP request payload fields |
Handling all conversation events while keeping prebuilt UI
For full event handling (node changes, bot speaking state, transcript updates, tool calls, and timeout), add a small sibling listener component that usesuseAkapuluEvents.
event.type:
status_changed
| Field | Type | Description |
|---|---|---|
type | "status_changed" | Discriminator |
status | "idle" | "connecting" | "connected" | "disconnecting" | "ended" | "error" | Session lifecycle state |
bot_speaking_state_changed
| Field | Type | Description |
|---|---|---|
type | "bot_speaking_state_changed" | Discriminator |
speakingState | "idle" | "speaking" | "listening" | Bot speaking/listening state |
node_changed
| Field | Type | Description |
|---|---|---|
type | "node_changed" | Discriminator |
node | { key: string; label: string } | null | Current flow node |
tool_event
| Field | Type | Description |
|---|---|---|
type | "tool_event" | Discriminator |
tool | NormalizedToolEvent | Normalized tool event payload |
transcript_updated
| Field | Type | Description |
|---|---|---|
type | "transcript_updated" | Discriminator |
transcript | TranscriptEntry | Updated transcript row |
call_ready
| Field | Type | Description |
|---|---|---|
type | "call_ready" | Call reached ready state |
timeout
| Field | Type | Description |
|---|---|---|
type | "timeout" | Discriminator |
reason | "duration_limit_reached" | "participant_join_timeout" | Timeout reason from backend |
Using @akapulu/react without @akapulu/react-ui
The same connect/updates route pattern applies: AkapuluProvider points at your local connectPath and updatesPath, your server uses @akapulu/server. What changes is UI: you build layout yourself and pull state from hooks instead of mounting AkapuluConversation.
How it fits together
- Wrap your tree in
AkapuluProvider(as in the Quickstart).
-
Call
useAkapuluSession()anywhere under that provider. It exposes everything in the session store plusstart/end: Lifecycle & connectionstatus— where the client is in the join/leave flow:"idle"→"connecting"→"connected"→"disconnecting"/"ended", or"error"if something failed.start/end— async actions that begin the conversation (connect + Daily join) or hang up and reset session state.error— whenstatusis"error", structured details (codeoptional,messagerequired) for your error UI or logging.
callIsReady— whether the backend considers the call ready (often used whilestatus === "connecting"so you are not stuck on a spinner forever).completionPercent— numeric progress through the scenario (0–100).latestUpdateText— short human-readable status line for loading/progress copy.
currentNode— the active scenario node ({ key, label }) ornullif none.
transcripts— ordered list of rows (id,text,speaker,timestamp,isFinal) for your own transcript UI.botSpeakingState—"idle","speaking", or"listening"for indicators or turn-taking UI.
conversationSessionId— Akapulu Labs conversation session id from connect (used when polling updates; also useful if your app logs or links out to dashboard/API records).
- The provider joins the Daily room for realtime media. Install
@daily-co/daily-react(and peer@daily-co/daily-js) and use its primitives to draw video—for exampleDailyVideo,useDaily, anduseVideoTrack. For assistant video selection, useuseAkapuluParticipantRolesfrom@akapulu/react.
- Use
useAkapuluMediaControls()for in-call mic and camera toggles (wired to that Daily session).
- Render
<AkapuluBotAudio />once in the tree so assistant audio plays (small hidden element; required for typical voice/video bots).
- Optionally
useAkapuluEvents(callback)to react to the full event stream (transcript_updated,tool_event,node_changed,timeout, etc.) while still rendering your own UI.
@akapulu/react
| Export | Role |
|---|---|
AkapuluProvider | Config, session store, Daily context for children |
useAkapuluSession | Session state + start / end |
useAkapuluMediaControls | Mic/cam state and toggles |
AkapuluBotAudio | Plays bot audio track |
useAkapuluEvents | Subscribe to AkapuluEvent stream |
useAkapuluDailyCall | Access the Daily call object when you need lower-level APIs |
end() from useAkapuluSession(). That leaves the Daily room from the browser and runs the SDK teardown so the session is no longer live. If the assistant disconnects from Daily first, AkapuluProvider calls end() for you so status does not stay connected with no assistant in the room.
Types and shared logic
Use AkapuluEvent from @akapulu/react. For TranscriptEntry, NormalizedToolEvent, and related handler shapes on AkapuluConversation, align with the prop types exported from @akapulu/react-ui.
Conversation detail and recording
Separate from the live call connect and updates loop,@akapulu/server can fetch post-call (or in-progress) conversation detail and recording responses from your backend—for example dedicated HTTP handlers your UI calls after conversation_session_id is known.
Conversation detail retrieval
@akapulu/server also exposes getConversationDetail(conversationSessionId) for fetching a completed or in-progress conversation detail payload from your backend.
ConversationDetailResponse from @akapulu/server):
| Field | Type | Description |
|---|---|---|
id | string | Conversation id |
created_at | string | Created timestamp |
created_at_text | string | Human-readable created time |
duration_text | string | Human-readable duration |
avatar | object | Avatar summary (see below) |
scenario_id | string | Scenario id |
recording | object | Recording metadata on this conversation (see below; not the downloadable file) |
transcript_rows | array | Ordered transcript rows (see below) |
avatar
| Field | Type | Description |
|---|---|---|
id | string | Avatar id |
href | string | Avatar resource URL |
profile_image_url | string | Profile image URL |
recording (detail payload)
| Field | Type | Description |
|---|---|---|
has_recording | boolean | Whether a recording exists |
status_text | string | Recording status for display |
is_ready | boolean | Whether the recording is ready to fetch |
transcript_rows[]
| Field | Type | Description |
|---|---|---|
role | string | Speaker / row role |
content | string (optional) | Row text |
node_id | string (optional) | Scenario node id |
tool_call_id | string (optional) | Tool call id |
tool_calls | unknown[] (optional) | Tool call payloads |
Conversation recording retrieval
@akapulu/server also exposes getConversationRecording(conversationSessionId) for fetching the recording response for a conversation from your backend.
ConversationRecordingResponse from @akapulu/server):
kind | Meaning |
|---|---|
"redirect" | Akapulu Labs responded with a redirect; follow location. |
"json" | Response body was JSON; see payload. |
"binary" | Raw bytes (for example a media file); see body and headers. |
kind: "redirect"
| Field | Type | Description |
|---|---|---|
kind | "redirect" | Discriminator |
status | number | HTTP status (3xx) |
location | string | Location URL |
kind: "json"
| Field | Type | Description |
|---|---|---|
kind | "json" | Discriminator |
status | number | HTTP status |
payload | unknown | Parsed JSON body |
kind: "binary"
| Field | Type | Description |
|---|---|---|
kind | "binary" | Discriminator |
status | number | HTTP status |
body | ArrayBuffer | Raw response bytes |
contentType | string | Content-Type |
contentDisposition | string | Content-Disposition |
Examples
Runnable apps that mirror this guide are available in Akapulu/akapulu-examples (fundamentals/). Both are Next.js apps with @akapulu/server route handlers for POST …/connect and GET …/updates?conversation_session_id=…
-
Pre-built UI —
fundamentals/prebuilt-ui:@akapulu/react(AkapuluProviderwith localconnectPath/updatesPath) plus@akapulu/react-ui(AkapuluConversationfor the full default layout: video, transcript, controls). Useful when you want to ship quickly and customize via props or CSS rather than rebuilding layout. -
Custom UI —
fundamentals/custom-ui:@akapulu/reactonly—same provider pattern, thenuseAkapuluSession,useAkapuluMediaControls,useAkapuluParticipantRoles,useAkapuluEvents,AkapuluBotAudio, and@daily-co/daily-react(DailyVideo, track hooks) for your own chrome. Useful when you need full control over markup and state wiring.

