Skip to main content
This guide is based on the Akapulu Labs Web SDK.

Packages

Install these:
  • @akapulu/server on your backend
  • @akapulu/react in the React app
  • @akapulu/react-ui only if you use the prebuilt AkapuluConversation
  • @daily-co/daily-react only if your own components import DailyVideo or Daily hooks

Quickstart (React)

AkapuluProvider takes paths to your local API routes:
  • connectPath: your local connect endpoint
  • updatesPath: your local updates endpoint
The SDK calls these local routes from the browser. Your server routes then call Akapulu Labs APIs using @akapulu/server.

Backend connect and updates routes

Your local connect route should:
  • call akapulu.connectConversation(...) with the Akapulu Labs connect schema
    • scenario_id: string
    • avatar_id: string
    • runtime_vars?: Record<string, string>
    • stt_keywords?: string[]
    • record_conversation?: boolean
    • show_ai_label?: boolean
  • return the connect response fields required by the SDK:
    • room_url
    • token
    • conversation_session_id
Your local updates route should:
  • read conversation_session_id from the query param
  • call akapulu.pollConversationUpdates(conversationSessionId)
  • return the updates payload fields required by the SDK:
    • call_is_ready
    • completion_percent
    • latest_update_text
    • conversation_session_id
The SDK always calls your updatesPath with conversation_session_id in the URL query string. It polls that route only while the avatar is booting, then stops. Authenticate those GETs with config.headers the same way you authenticate connect. Install @akapulu/server in your backend and create two routes:
  • POST /api/akapulu/connect
  • GET /api/akapulu/updates?conversation_session_id=...
Then point AkapuluProvider to those local paths:
  • connectPath: "/api/akapulu/connect"
  • updatesPath: "/api/akapulu/updates"

Browser vs server

  • In the browser: AkapuluProvider (via config.endpoints.connectPath and config.endpoints.updatesPath) requests your connect and updates backend routes.
  • On your server: those routes use @akapulu/server, read the Akapulu Labs API key from a 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.
Authenticate users on your connect and updates routes the same way you authenticate the rest of your product. Connect starts a billed conversation, so only authenticated users should be able to start a call. Pass those auth headers with config.headers (object or function); do not put them in connectBody. The SDK sends those headers on the connect POST and on each boot-time updates GET. AkapuluProvider expects your local connect and updates endpoints to return the JSON schema described under Backend connect and updates routes above.

Passing a body and headers to your local connect route

AkapuluProvider can send JSON to your local connectPath via config.connectBody, and headers on connect and updates via config.headers. Each can be a plain object, or a function that is called when the request is sent (not when the provider mounts). Use a function when the value can change or you need to load it asynchronously — for example an auth token. getAccessToken below is your own helper.
  • If connectBody is provided, the SDK sends it as the POST JSON body to your local connect route.
  • If connectBody is omitted, the SDK sends POST /connectPath with no body.
  • If headers is provided, those headers are sent on the connect POST and on each updates GET.
config has no API key field and no fetch interceptor. User auth is headers; connectBody is payload only. Functions for connectBody and headers run when the request is sent. endpoints and transport stay from first mount. The full AkapuluConfig type is on the React SDK page. Frontend example:
Backend example (your local connect route):

Styling AkapuluConversation

@akapulu/react-ui supports slot-level customization via:
  • className for the root
  • classes for slot class names
  • styles for 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 a data-slot marker so users can inspect and target it in DevTools without guessing.

Layout

Loading

Error modal

Tool events

Video + controls

Transcript

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 rows
  • renderTranscriptEntry(entry) to render transcript rows with your own JSX
  • onToolEvent(tool) to run side effects when a tool event arrives
  • renderToolEvent(tool) to replace the default tool toast element
  • toolEventTimeoutMs to control how long the tool toast stays visible
  • playConnectChime to play a short chime when the call goes live (default true; pass false to disable)
Callback signatures:
toolEventTimeoutMs behavior:
  • default: 4000
  • pass a number to customize the auto-hide timeout
  • pass null to disable auto-hide
playConnectChime behavior:
  • default: true
  • pass false to disable
  • plays when session status goes connectingconnected (loading screen ends)
  • skipped when prefers-reduced-motion: reduce
TranscriptEntry shape (entry): NormalizedToolEvent shape (tool) by tool type:

RAG tool event

vision tool event

http tool event

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 uses useAkapuluEvents.
Event schema by event.type:

status_changed

bot_speaking_state_changed

node_changed

tool_event

transcript_updated

call_ready

timeout

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
  1. Wrap your tree in AkapuluProvider (as in the Quickstart).
  1. Call useAkapuluSession() anywhere under that provider. It exposes everything in the session store plus start / end: Lifecycle & connection
    • status — 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. start() also arms AudioContext during the Start Call click so useConnectChime() can play after a long boot.
    • error — when status is "error", structured details (code optional, message required) for your error UI or logging.
    Call readiness & updates (from your updates route)
    • callIsReady — whether the backend considers the call ready (often used while status === "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.
    Scenario / flow
    • currentNode — the active scenario node ({ key, label }) or null if none.
    Transcript & bot
    • 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.
    Correlation
    • 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).
  1. The provider joins the Daily room for realtime media. @akapulu/react already depends on @daily-co/daily-react. Add it as a direct dependency too if you import DailyVideo, useDaily, or useVideoTrack in your own files (so the import is yours, not a nested package). For assistant video selection, use useAkapuluParticipantRoles from @akapulu/react.
  1. Use useAkapuluMediaControls() for in-call mic and camera toggles (wired to that Daily session).
  1. Render <AkapuluBotAudio /> once in the tree so assistant audio plays (small hidden element; required for typical voice/video bots).
  1. Call useConnectChime() to play the same connect chime as AkapuluConversation when status goes connectingconnected. Skip this hook if you do not want a chime.
  1. Optionally useAkapuluEvents(callback) to react to the full event stream (transcript_updated, tool_event, node_changed, timeout, etc.) while still rendering your own UI.
Rough outline
Main exports from @akapulu/react Ending a call: When your user ends the call in your app, call 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.
JSON body shape (matches ConversationDetailResponse from @akapulu/server):

avatar

recording (detail payload)

transcript_rows[]

Conversation recording retrieval

@akapulu/server also exposes getConversationRecording(conversationSessionId) for fetching the recording response for a conversation from your backend.
Return value shape (discriminated union, ConversationRecordingResponse from @akapulu/server):

kind: "redirect"

kind: "json"

kind: "binary"

Examples

Runnable apps that mirror this guide are available as standalone Vite + Express repos. Each has an Express backend/ with @akapulu/server route handlers for POST /api/connect and GET /api/updates?conversation_session_id=…, plus a React frontend/.
  • Prebuilt UIAkapulu/prebuilt-ui: @akapulu/react (AkapuluProvider with local connectPath / updatesPath) plus @akapulu/react-ui (AkapuluConversation for the full default layout: video, transcript, controls). Useful when you want to ship quickly and customize via props or CSS rather than rebuilding layout.
  • Prebuilt UI (styled)Akapulu/prebuilt-ui-styled: the same prebuilt path with a dark theme, a custom tool toast, and a post-call review screen (recording + transcript).
  • Customized UIAkapulu/customized-ui: @akapulu/react only—same provider pattern, then useAkapuluSession, useAkapuluMediaControls, useAkapuluParticipantRoles, useConnectChime, 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.