Documentation Index
Fetch the complete documentation index at: https://docs.akapulu.com/llms.txt
Use this file to discover all available pages before exploring further.
This guide is based on the Akapulu Web SDK.
Packages
@akapulu/react
@akapulu/react-ui
@akapulu/server
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 APIs using @akapulu/server.
import { AkapuluProvider } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";
export function App() {
return (
<AkapuluProvider
config={{
endpoints: {
connectPath: "/api/akapulu/connect",
updatesPath: "/api/akapulu/updates",
},
}}
>
<AkapuluConversation title="Akapulu demo" />
</AkapuluProvider>
);
}
Backend connect and updates routes
Your local connect route should:
- call
akapulu.connectConversation(...) with the Akapulu connect schema
scenario_id: string
avatar_id: string
runtime_vars?: Record<string, string>
stt_keywords?: string[]
record_conversation?: 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.
Install @akapulu/server in your backend and create two routes:
POST /api/akapulu/connect
GET /api/akapulu/updates?conversation_session_id=...
import { createAkapuluServerClient } from "@akapulu/server";
const akapulu = createAkapuluServerClient();
// POST /api/akapulu/connect
export async function connectRoute() {
return await akapulu.connectConversation({
scenario_id: "YOUR_SCENARIO_ID",
avatar_id: "YOUR_AVATAR_ID",
runtime_vars: {},
record_conversation: false,
});
}
// GET /api/akapulu/updates?conversation_session_id=...
export async function updatesRoute(conversationSessionId: string) {
return await akapulu.pollConversationUpdates(conversationSessionId);
}
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 API key from server-side environment variable, and call Akapulu’s 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
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.
Frontend example:
import { AkapuluProvider } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";
export function App() {
return (
<AkapuluProvider
config={{
endpoints: {
connectPath: "/api/akapulu/connect",
updatesPath: "/api/akapulu/updates",
},
connectBody: {
tenant_id: "acme-health",
patient_id: "patient_001",
should_record: true,
},
}}
>
<AkapuluConversation title="Akapulu demo" />
</AkapuluProvider>
);
}
Backend example (your local connect route):
import { createAkapuluServerClient } from "@akapulu/server";
const akapulu = createAkapuluServerClient();
type LocalConnectBody = {
tenant_id?: string;
patient_id?: string;
should_record?: boolean;
};
export async function connectRoute(body: LocalConnectBody) {
return await akapulu.connectConversation({
scenario_id: "YOUR_SCENARIO_ID",
avatar_id: "YOUR_AVATAR_ID",
runtime_vars: {
patient_id: body.patient_id || "patient_001",
tenant_id: body.tenant_id || "default_tenant",
},
record_conversation: body.should_record === true,
});
}
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.
<AkapuluConversation
title="Akapulu demo"
classes={{
transcriptContainer: "myTranscriptContainer",
controlEnd: "myLeaveButton",
}}
styles={{
videoPane: { borderRadius: 20 },
connectedLayout: { gap: "1.5rem" },
}}
/>
Example 2: Default class overrides (global CSS)
Use built-in default classes when you want to apply theme-like global styles from CSS.
.akapulu-transcript-container {
border: 1px solid #334155;
border-radius: 14px;
}
.akapulu-control-end {
background: rgba(185, 28, 28, 0.9);
}
Example 3: data-slot overrides (stable CSS selectors)
Use data-slot selectors when you want explicit, inspectable selectors in DevTools.
[data-slot="transcript-header"] {
backdrop-filter: blur(2px);
}
[data-slot="pip"] {
width: 28%;
border-color: #60a5fa;
}
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
| 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 |
| 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 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
Callback signatures:
transcriptFilter?: (entry: TranscriptEntry) => boolean;
renderTranscriptEntry?: (entry: TranscriptEntry) => ReactNode;
onToolEvent?: (tool: NormalizedToolEvent) => void;
renderToolEvent?: (tool: NormalizedToolEvent) => ReactNode;
toolEventTimeoutMs?: number | null;
toolEventTimeoutMs behavior:
- default:
4000
- pass a number to customize the auto-hide timeout
- pass
null to 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:
| Field | Type | Description |
|---|
messageType | "RAG" | RAG tool event |
functionName | string | RAG function/tool name |
summary | string | "RAG tool called" |
query | string | undefined | Query text |
| Field | Type | Description |
|---|
messageType | "vision" | Vision tool event |
functionName | string | Vision function/tool name |
summary | string | "Vision tool called" |
| 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 |
import { AkapuluProvider } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";
export function App() {
return (
<AkapuluProvider config={config}>
<AkapuluConversation
title="Akapulu demo"
transcriptFilter={(entry) => {
// Example: skip rows you do not want in the transcript list
return entry.text.trim() !== "skip me";
}}
renderTranscriptEntry={(entry) => (
<div>
<strong>{entry.speaker === "user" ? "You" : "Bot"}:</strong> {entry.text}
</div>
)}
onToolEvent={(tool) => {
// Example: analytics side effect
console.log("tool_event", tool.messageType, tool.functionName);
}}
renderToolEvent={(tool) => (
<div style={{ border: "1px solid #334155", borderRadius: 8, padding: 12 }}>
<strong>{tool.summary}</strong>
<div style={{ marginTop: 6, opacity: 0.85 }}>{tool.functionName}</div>
</div>
)}
/>
</AkapuluProvider>
);
}
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.
import { AkapuluProvider, useAkapuluEvents } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";
function ConversationEventListener() {
useAkapuluEvents((event) => {
// … handle `event` (discriminated by `event.type`; see schema below)
});
return null;
}
export function App() {
return (
<AkapuluProvider
config={{
endpoints: {
connectPath: "/api/akapulu/connect",
updatesPath: "/api/akapulu/updates",
},
}}
>
<AkapuluConversation title="Akapulu demo" />
<ConversationEventListener />
</AkapuluProvider>
);
}
Event schema by 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 |
| 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 |
import { AkapuluProvider, useAkapuluEvents } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";
function ConversationEventBridge() {
useAkapuluEvents((event) => {
if (event.type === "transcript_updated") {
// event.transcript
return;
}
if (event.type === "bot_speaking_state_changed") {
// event.speakingState: "idle" | "speaking" | "listening"
return;
}
if (event.type === "node_changed") {
// event.node
return;
}
if (event.type === "tool_event") {
// event.tool
return;
}
if (event.type === "timeout") {
// event.reason
return;
}
});
return null;
}
export function App() {
return (
<AkapuluProvider config={config}>
<ConversationEventBridge />
<AkapuluConversation title="Akapulu demo" />
</AkapuluProvider>
);
}
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).
<AkapuluProvider
config={{
endpoints: {
connectPath: "/api/akapulu/connect",
updatesPath: "/api/akapulu/updates",
},
}}
>
{/* your UI */}
</AkapuluProvider>
-
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.
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 conversation session id from connect (used when polling updates; also useful if your app logs or links out to dashboard/API records).
const {
status,
start,
end,
error,
callIsReady,
conversationSessionId,
completionPercent,
latestUpdateText,
currentNode,
transcripts,
botSpeakingState,
} = useAkapuluSession();
- 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 example DailyVideo, useDaily, useParticipantIds, and useVideoTrack.
import { DailyVideo, useDaily, useParticipantIds, useVideoTrack } from "@daily-co/daily-react";
function BotVideoTile() {
const daily = useDaily();
const [sessionId] = useParticipantIds({ filter: "remote" });
const track = useVideoTrack(sessionId);
if (track.isOff) return null;
return <DailyVideo sessionId={sessionId} type="video" />;
}
function SelfVideoTile() {
const daily = useDaily();
const sessionId = daily?.participants().local.session_id ?? "";
const track = useVideoTrack(sessionId);
if (!sessionId || track.isOff) return null;
return <DailyVideo sessionId={sessionId} type="video" />;
}
- Use
useAkapuluMediaControls() for in-call mic and camera toggles (wired to that Daily session).
const { toggleMic, toggleCam, isMicMuted, isCamOff } = useAkapuluMediaControls();
<button type="button" onClick={() => void toggleMic()}>
Mic {isMicMuted ? "unmute" : "mute"}
</button>
<button type="button" onClick={() => void toggleCam()}>
Cam {isCamOff ? "on" : "off"}
</button>
- 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.
useAkapuluEvents((event) => {
// … handle `event`
});
Rough outline
"use client";
import { AkapuluBotAudio, AkapuluProvider, useAkapuluEvents, useAkapuluMediaControls, useAkapuluSession } from "@akapulu/react";
import { DailyVideo, useDaily, useParticipantIds, useVideoTrack } from "@daily-co/daily-react";
// Single participant video tile; Daily hooks are available because `AkapuluProvider` wraps Daily context.
function VideoTile(props: { sessionId: string; isLocal: boolean }) {
// Track state for this participant’s video in the current Daily call.
const videoTrack = useVideoTrack(props.sessionId);
if (videoTrack.isOff) {
// TODO: render your own “camera off” placeholder here.
return null;
}
// `sessionId` must be this participant’s Daily `session_id` (from `useParticipantIds` or `participants().local`).
return <DailyVideo sessionId={props.sessionId} type="video" />;
}
function CustomCallUi() {
// Akapulu session: status, lifecycle controls, transcript + progress state.
const {
status,
start,
end,
error,
transcripts,
completionPercent,
latestUpdateText,
botSpeakingState,
currentNode,
} = useAkapuluSession();
// Media controls for the underlying Daily call.
const { toggleMic, toggleCam, isMicMuted, isCamOff } = useAkapuluMediaControls();
// Daily participant list: remote ids + local self.
const daily = useDaily();
// Daily ids for everyone except this browser client.
// In the 1:1 Akapulu call, remoteParticipantIds[0] is the bot video participant.
const remoteParticipantIds = useParticipantIds({ filter: "remote" });
// Local browser participant (the user/self-view tile).
const localParticipant = daily?.participants().local;
const localId = localParticipant?.session_id ?? "";
// Subscribe to high-level conversation events (status, tools, transcript, node, timeouts).
useAkapuluEvents((event) => {
if (event.type === "status_changed") {
// handle session lifecycle changes (idle/connecting/connected/ended/error)
return;
}
if (event.type === "tool_event") {
// handle RAG / HTTP / vision tool activity
return;
}
if (event.type === "transcript_updated") {
// handle incremental transcript updates
return;
}
if (event.type === "node_changed") {
// handle scenario node changes
return;
}
if (event.type === "bot_speaking_state_changed") {
// handle bot speaking/listening/idle state
return;
}
if (event.type === "timeout") {
// handle duration / participant_join timeouts
return;
}
});
// Render behavior by session status:
// - error -> dedicated error surface
// - idle / ended -> pre-call surface (+ start button)
// - connecting -> loading/progress surface
// - connected -> full in-call surface (video, controls, transcript)
switch (status) {
case "error":
return (
<>
{/* TODO: dedicated error UI*/}
<AkapuluBotAudio />
</>
);
case "idle":
case "ended":
return (
<>
{/* TODO: pre-call UI (title, consent, etc.) */}
<button type="button" onClick={() => void start()}>
Start call
</button>
<AkapuluBotAudio />
</>
);
case "connecting":
return (
<>
<p>Connecting...</p>
{/* TODO: replace with your spinner component */}
<div>Spinner</div>
<progress value={completionPercent} max={100} />
<p>{latestUpdateText}</p>
<AkapuluBotAudio />
</>
);
case "connected":
return (
<>
{/* TODO: in-call header (botSpeakingState, currentNode, etc.) */}
{/* Bot video (remote participant) */}
{remoteParticipantIds[0] ? (
<VideoTile sessionId={remoteParticipantIds[0]} isLocal={false} />
) : (
<>{/* TODO: waiting-for-bot-video placeholder */}</>
)}
{/* User/self-view video (local participant from this browser). */}
{localParticipant ? (
<VideoTile sessionId={localParticipant.session_id} isLocal />
) : (
<>{/* TODO: waiting-for-user-video placeholder */}</>
)}
{/* Call controls */}
<>
<button onClick={toggleMic}>
{isMicMuted ? "Unmute" : "Mute"}
</button>
<button onClick={toggleCam}>
{isCamOff ? "Camera on" : "Camera off"}
</button>
<button onClick={() => void end()}>
Leave
</button>
</>
{/* Transcript surface */}
<>
{transcripts.map((entry) => {
// TODO: return your transcript row component (use key={entry.id}).
// TODO: use entry.speaker / entry.text / entry.isFinal for row content + styling.
return null;
})}
</>
<AkapuluBotAudio />
</>
);
}
}
export function App() {
return (
<AkapuluProvider
config={{
endpoints: {
connectPath: "/api/akapulu/connect",
updatesPath: "/api/akapulu/updates",
},
// Optional connectBody — see "Passing custom payload" above.
}}
>
<CustomCallUi />
</AkapuluProvider>
);
}
Main exports from @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 |
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.
import { createAkapuluServerClient } from "@akapulu/server";
const akapulu = createAkapuluServerClient();
// GET /api/akapulu/conversation-details?conversation_session_id=...
export async function conversationDetailsRoute(conversationSessionId: string) {
return await akapulu.getConversationDetail(conversationSessionId);
}
JSON body shape (matches 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.
import { createAkapuluServerClient } from "@akapulu/server";
const akapulu = createAkapuluServerClient();
// GET /api/akapulu/view-recording?conversation_session_id=...
export async function recordingRoute(conversationSessionId: string) {
return await akapulu.getConversationRecording(conversationSessionId);
}
Return value shape (discriminated union, ConversationRecordingResponse from @akapulu/server):
kind | Meaning |
|---|
"redirect" | Akapulu 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 (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.
-
Custom UI —
fundamentals/custom-ui: @akapulu/react only—same provider pattern, then useAkapuluSession, useAkapuluMediaControls, useAkapuluEvents, AkapuluBotAudio, and @daily-co/daily-react (DailyVideo, participant/track hooks) for your own chrome. Useful when you need full control over markup and state wiring.