BURKI WEBSITE VOICE INTEGRATION KIT Checked October 6, 2026. SDK @burki.dev/sdk 0.2.0. Original guide: https://burki.dev/blog/burki-javascript-voice-agent-website This free resource is a controller scaffold and integration worksheet. Production voice sessions are not free. You must implement your application's real authentication, authorization, persistent grant store, protected server routes and UI. Nothing below grants anonymous access to a Burki organization's assistants. No paid call or provider action was performed to prepare it. 1. CHOOSE THE FIRST SCOPE Application: __________________________ Authenticated users allowed to use voice: __________________________ Allowed assistant selected by SERVER policy: __________________________ Maximum duration (authenticated API supports integer 1–300 seconds): ______ Approved instructions/version: __________________________ Usage owner / allowance presentation: __________________________ Provider readiness/funding check: __________________________ Recording off initially: yes / no (if no, review extra prerequisites) Text/human fallback: __________________________ Sensitive information excluded: __________________________ Server secret storage location (do not put the key in this sheet): __________ Fictional worked configuration: Cedar Service Portal; logged-in customer; assistant 42 allowed by app policy; 120 seconds; recordingConsent false. Explain approved portal procedures and capture a question. No CRM write/calendar action. 2. SERVER ROUTE CONTRACT (YOU IMPLEMENT THESE APP ROUTES) Use server-only import { BurkiClient } from '@burki.dev/sdk'. Initialize with a server-held BURKI_API_KEY owned by the intended verified Burki account/organization. Your app's identity is separate from the Burki API identity. The SDK official server example is at: https://github.com/meeran03/burki-js-sdk/blob/main/examples/browser-server.ts POST /voice/session - Authenticate the real app session and validate CSRF; don't trust owner ID JSON. - Resolve assistant ID from current app-owned access rules. - Set duration, recording choice, variables and purpose on the server. - Create immutable UUID input; persist { ownerId, input } DURABLY before API use. - Run burki.browser.preflight(input), return only handle and safe allowance. - Do not submit start here, silently change test purpose or promise reserved funds. POST /voice/session/:requestId/start - Authenticate, validate CSRF, find grant, check owner and current assistant access. - Pass saved immutable input to burki.browser.start(input). - Forward actual HTTP errors without dropping status or leaking credentials. - Return admitted session credentials only to the authorized app user. POST /voice/session/:requestId/stop - Repeat authentication, CSRF, owner/current-access checks. - Stop this exact request's callSid; retain grant for reconciliation. - SDK server example derives browser_call_LK plus UUID without hyphens. GET /voice/session/:requestId/status - Repeat authentication and owner/current-access checks. - Return this session's status including settled, not a generic active flag. Every route: Cache-Control: no-store; appropriate app rate limits; no unrestricted Burki body/endpoint proxy; no long-lived key in response/logs/frontend. A custom transport must preserve status through SessionRequestError. Handle timeouts and unknown outcomes against the original request; never create a fresh ID as an automatic fallback. Changed settings require a new grant and fresh preflight. 3. COMPLETE BROWSER CONTROLLER SCAFFOLD (TypeScript) Install: npm install @burki.dev/sdk@0.2.0 livekit-client@2.22.2 The app prepares a handle with the protected POST /voice/session route, displays its allowance, and then passes that handle into this controller. The caller's UI implements these hooks. Buttons call start/mute/unmute/resume/stop explicitly. No action below starts automatically when the controller is created. --- begin website-voice-controller.ts --- import { createBrowserCall, SessionRequestError, type SessionCredentials, } from '@burki.dev/sdk/browser'; type Handle = { request_id: string; assistant_id: number; allowance: { eligible: boolean; blockers: Array<{ message: string }> }; }; type UI = { setState: (state: string) => void; upsertTranscript: (part: { id: string; speaker: 'user' | 'assistant'; text: string; final: boolean; }) => void; showResumeAudio: (show: boolean) => void; showError: (message: string) => void; }; export function websiteVoiceController(handle: Handle, csrfToken: string, ui: UI) { if (!handle.allowance.eligible) { throw new Error(handle.allowance.blockers[0]?.message ?? 'Voice is unavailable'); } const base = `/voice/session/${encodeURIComponent(handle.request_id)}`; async function request(action: 'start' | 'stop' | 'status'): Promise { const response = await fetch(`${base}/${action}`, { method: action === 'status' ? 'GET' : 'POST', credentials: 'same-origin', headers: action === 'status' ? {} : { 'X-CSRF-Token': csrfToken }, cache: 'no-store', }); const body = await response.json().catch(() => ({})); if (!response.ok) { throw new SessionRequestError(body.message ?? 'Voice request failed', response.status); } return body as T; } const call = createBrowserCall({ input: { request_id: handle.request_id, assistant_id: handle.assistant_id }, transport: { start: () => request('start'), stop: () => request('stop'), status: () => request<{ settled?: boolean }>('status'), }, callbacks: { progress: stage => ui.setState(stage), ready: () => ui.setState('ready'), transcript: (id, speaker, text, final) => ui.upsertTranscript({ id, speaker, text, final: !!final }), ended: settled => ui.setState(settled ? 'ended' : 'ended_pending_review'), error: message => ui.showError(message), playbackBlocked: blocked => ui.showResumeAudio(blocked), warning: message => ui.showError(message), }, }); return { start: () => call.start(), mute: () => call.setMuted(true), unmute: () => call.setMuted(false), resume: () => call.resumeAudio(), stop: () => call.stop(), status: () => request<{ settled?: boolean }>('status'), }; } --- end website-voice-controller.ts --- UI wiring worksheet: Prepare voice: obtain protected handle and show allowance/limits/blockers. Start voice: controller.start() from a user click; show preparing until ready. Mute/unmute: controller.mute()/unmute(). Resume audio: visible only when playbackBlocked; controller.resume() on user click. End conversation: await controller.stop(); false means pending review/reconciliation. Transcript: replace each segment by ID; don't append repeated partial text. Disabled/expired access: no call; provide text/human fallback. Page unload: local media may stop; server grant persists for status/stop reconciliation. 4. ACCEPTANCE WORKSHEET Use mocked transports first; authorized live acceptance is a separate decision. For each case record expected behavior, observed result, exact evidence and correction. Case 1: User denies microphone. Expected: no false ready state; helpful permission error/fallback; no new request retry. Observed: ______ Evidence: ______ Correction: ______ Case 2: App session expired before start. Expected: server rejects unauthorized user; HTTP status preserved; sign-in option. Observed: ______ Evidence: ______ Correction: ______ Case 3: Different user requests an existing grant. Expected: grant ownership check rejects; no token or other user review disclosed. Observed: ______ Evidence: ______ Correction: ______ Case 4: Preflight funding/configuration blocker. Expected: blocker shown before start; no claim funds reserved; no silent paid fallback. Observed: ______ Evidence: ______ Correction: ______ Case 5: Start accepted but agent never ready. Expected: preparing then honest failure; no success based only on REST 200. Observed: ______ Evidence: ______ Correction: ______ Case 6: Browser blocks playback. Expected: Resume audio user control; no hidden autoplay loop or silent success. Observed: ______ Evidence: ______ Correction: ______ Case 7: Same transcript segment arrives partial then final. Expected: one updated segment by ID; final flag retained; no duplicate paragraph. Observed: ______ Evidence: ______ Correction: ______ Case 8: Stop returns false/response uncertain. Expected: local mic stopped; ended pending review; reconcile original ID through server status. Observed: ______ Evidence: ______ Correction: ______ Case 9: Membership changes after grant created. Expected: every route rechecks current assistant access; old grant does not bypass policy. Observed: ______ Evidence: ______ Correction: ______ Case 10: Duration/consent/settings change. Expected: intentional new immutable grant and preflight, not mutation of old request. Observed: ______ Evidence: ______ Correction: ______ 5. RELEASE READINESS Auth/CSRF/tenant binding review owner: ______ HTTP status mapping/credential privacy inspected: ______ Website HTTPS / iframe permission policy inspected: ______ SDK version / lockfile: ______ UI mocked acceptance completed: ______ Allowed call acceptance receipt if later performed: ______ Current pricing/funding/limits reviewed: ______ Exact website release: ______ Open issues and fallback owner: ______ Sources: https://github.com/meeran03/burki-js-sdk https://registry.npmjs.org/@burki.dev/sdk/0.2.0 https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay https://burki.dev/pricing