VoxPro Developer Docs
Everything you need to build, launch, and integrate AI voice assistants — outbound campaigns, inbound routing, live tool calls, post-call webhooks, and more.
Introduction
VoxPro is a real-time AI voice platform. An Assistant defines how the AI sounds and behaves; a Call instantiates that assistant for a specific customer. Calls run on LiveKit rooms connected via SIP trunks.
Assistant
Knowledge
Call / Blast
Automations
Logs
Key concepts
Assistant
A saved configuration: voice, LLM model, system prompt, first message, tools, and knowledge base. Identified by a unique id.
Room
A LiveKit room created per call. roomName is the primary key used in call logs, campaign jobs, and recordings.
Blast Campaign
A set of outbound calls placed concurrently from a CSV list, with optional automatic retry on unanswered / voicemail.
Webhook Tool
An HTTP endpoint you own. The AI calls it mid-conversation to look up data or trigger actions. You return a plain-text response.
Authentication
All API endpoints require a Bearer token in the Authorization header. Obtain one by logging in via the VoxPro UI — the token is stored in localStorage as voxpro.session.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
const session = JSON.parse(localStorage.getItem("voxpro.session") || "null");
const token = session?.token;
const res = await fetch("/assistants", {
headers: { Authorization: `Bearer ${token}` }
});
const { assistants } = await res.json();
Authorization header — omit Content-Type so the browser sets the correct multipart/form-data boundary automatically.Roles
| Role | Access |
|---|---|
| admin | Full access to all assistants, campaigns, logs, tools, customers, and sounds. |
| user / client | Access scoped to their organization's assistants and logs. |
Base URL & Headers
https://<your-voxpro-host>
# e.g. https://ai.yourcompany.com
| Header | Value | Required |
|---|---|---|
| Authorization | Bearer <token> | required |
| Content-Type | application/json | For JSON bodies |
| Content-Type | omit (let browser set it) | For file uploads |
audio/wav with Range support).Assistants — Overview
An Assistant is the blueprint for every AI voice call. It stores voice, LLM, prompt, knowledge, tools, and recording settings in one object persisted to disk (or Postgres if configured).
Assistants — Configuration Fields
Identity
| Field | Type | Description |
|---|---|---|
| id | string | Auto-generated unique ID (read-only after creation) |
| name | string | Internal identifier name |
| displayName | string | Shown to callers / UI |
| industry | string | e.g. "finance", "healthcare" |
| clientId | string|null | Links assistant to a customer allocation |
LLM (Language Model)
| Field | Type | Default | Description |
|---|---|---|---|
| llmProvider | string | "ytl" | openai · ytl · anthropic · local |
| llmModel | string | "ilmu-text-free-v2" | Model ID for the chosen provider |
| systemPrompt | string | — | Full system prompt. Supports {{variable}} template syntax |
| firstMessage | string | — | What the agent says first. Also supports {{variable}} |
| temperature | number | 0.4 | LLM temperature (0.0–2.0) |
| maxTokens | number | 220 | Max tokens per LLM reply |
| useCase | string | "voiceAssistant" | Selects default system prompt template if none set |
| language | string | "en" | ISO 639-1 language code |
ASR (Speech Recognition)
| Field | Type | Description |
|---|---|---|
| asrProvider | string | ytl · deepgram · openai · mesolitica |
| vadSensitivity | number | Voice activity detection threshold (0.0–1.0). Default: 0.62 |
| minUtteranceMs | number | Minimum speech length before transcribing (ms). Default: 240 |
| silenceThresholdMs | number | Silence duration before ending an utterance (ms). Default: 650 |
TTS (Voice / Speech Synthesis)
| Field | Type | Description |
|---|---|---|
| voiceProvider | string | ytl · openai · elevenlabs · edge · mesolitica · kokoro · others |
| voice | string | Voice ID from GET /voices. For ytl: voice_1/voice_2/voice_3 |
| voiceSettings.stability | number | ElevenLabs stability (0.0–1.0). Default: 0.45 |
| voiceSettings.similarity | number | ElevenLabs similarity boost. Default: 0.82 |
| voiceSettings.speed | number | Speaking rate multiplier. Default: 0.96 |
| voiceSettings.style | number | ElevenLabs style exaggeration. Default: 0.2 |
| backgroundNoise | string | Filename from sounds library (e.g. "office_ambient.mp3") |
| backgroundNoiseVolume | number | Mix volume 0.0–1.0. Default: 0.10 |
Call behaviour
| Field | Type | Default | Description |
|---|---|---|---|
| allowInterruption | boolean | true | Can the caller speak over the agent? |
| autoEndOnHangup | boolean | true | End the agent room when SIP call disconnects |
| autoEndOnSilenceSec | number | 60 | Hang up after N seconds of silence |
Tools & Knowledge
| Field | Type | Description |
|---|---|---|
| selectedToolIds | string[] | List of toolId slugs the agent may use during calls |
| csvLabel | string | Human label for the campaign CSV (shown in dynamic context) |
| csvFileName | string | Original filename of the campaign CSV |
| promptData | object | Extra key/value pairs merged into every call's template context |
Assistants — REST API
Returns assistants scoped to the authenticated user's organization (or all for admin).
{
"ok": true,
"assistants": [
{
"id": "asst_abc123",
"name": "Maya",
"displayName": "Maya — Xanderia",
"voiceProvider": "ytl",
"voice": "voice_1",
"llmProvider": "ytl",
"firstMessage": "Selamat datang! Saya Maya...",
"systemPrompt": "You are Maya...",
"selectedToolIds": ["crm-lookup"],
"createdAt": "2025-06-01T10:00:00.000Z",
"updatedAt": "2025-06-15T08:22:00.000Z"
}
]
}
{
"name": "Maya",
"displayName": "Maya — Loan Advisor",
"llmProvider": "ytl",
"llmModel": "ilmu-text-free-v2",
"asrProvider": "ytl",
"voiceProvider": "ytl",
"voice": "voice_1",
"language": "ms",
"temperature": 0.4,
"maxTokens": 220,
"firstMessage": "Selamat datang! Saya Maya dari Xanderia.",
"systemPrompt": "Anda adalah Maya...\n\n{{knowledge_base}}",
"selectedToolIds": [],
"allowInterruption": true,
"autoEndOnSilenceSec": 60
}
{ "ok": true, "assistant": { "id": "asst_abc123", "..." } }
Send only the fields you want to change. All other fields are preserved.
{ "systemPrompt": "You are Maya v2. {{kb_xanderia_faq}}" }
Creates a copy with the name appended with (copy). Returns the new assistant object.
{ "ok": true, "assistant": { "id": "asst_xyz789", "name": "Maya (copy)", "..." } }
{ "ok": true }
Template Variables
Use {{variable_name}} in systemPrompt and firstMessage. Variables are replaced at call-start with values from the campaign CSV row, the assistant's promptData, and injected system data.
Always-available variables
{{destination}}Dialed number (outbound){{customerNumber}}Alias for {{destination}}{{customer_phone}}Caller number (inbound){{customer_name}}Customer name from CSV{{channel}}voice{{knowledge_base}}All uploaded KB files concatenatedKnowledge base per-file variables
Each file uploaded to the Knowledge tab also gets its own variable, named kb_ + filename (lowercased, non-alphanumeric chars become underscores).
{{kb_xanderia_faq}}From xanderia_faq.csv{{kb_pricing}}From pricing.csv{{kb_procedures}}From procedures.txtCampaign CSV columns
Every column in the blast CSV row is injected automatically. If your CSV has a LoanAmount column:
The customer's outstanding loan amount is RM {{LoanAmount}}.
Their account reference is {{AccountRef}}.
Example — full system prompt with variables
Anda adalah Maya, penasihat pinjaman Xanderia.
Pelanggan: {{customer_name}} ({{destination}})
--- FAQ ---
{{kb_xanderia_faq}}
--- Senarai Harga ---
{{kb_pricing}}
Jawab dalam Bahasa Malaysia sahaja.
Knowledge Base — Overview
Upload CSV or TXT files per assistant. At call-start, all files are read from disk and injected into the LLM prompt as {{knowledge_base}} (all files) or {{kb_<filename>}} (individual file).
Supported formats
| Format | Parsing | Variable name |
|---|---|---|
| .csv | 2-column → Q/A pairs. 3+ columns → key: value blocks per row | kb_<name> |
| .txt | Raw text content | kb_<name> |
| .docx / .pdf / .xlsx | Stored but not currently parsed (binary) | — |
CSV format — 2-column FAQ
Question,Answer
"What is the minimum salary?","RM 2,000 per month"
"Maximum loan amount?","Up to RM 150,000"
"How long to approve?","3–5 working days"
Gets rendered in the prompt as:
Q: What is the minimum salary?
A: RM 2,000 per month
Q: Maximum loan amount?
A: Up to RM 150,000
Knowledge Base — REST API
{
"files": [
{
"name": "xanderia_faq.csv",
"size": 3840,
"uploadedAt": "2025-06-20T08:00:00.000Z"
}
]
}
Upload up to 20 files at once. Max 10 MB each. Field name: files.
const fd = new FormData();
fd.append("files", myFile); // can append multiple
const res = await fetch(`/knowledge/${assistantId}/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: fd // DO NOT set Content-Type manually
});
const { files } = await res.json();
{ "ok": true, "files": [ /* remaining files */ ] }
Using {{kb_*}} in System Prompts
The variable name is derived from the filename: lowercase, extension stripped, non-alphanumeric characters replaced with underscores.
| Uploaded file | Template variable |
|---|---|
| xanderia_faq_knowledge_base.csv | {{kb_xanderia_faq_knowledge_base}} |
| Pricing List 2025.csv | {{kb_pricing_list_2025}} |
| procedures.txt | {{kb_procedures}} |
| Product FAQ (EN).csv | {{kb_product_faq_en_}} |
Multiple knowledge bases in one prompt
## PRODUCT FAQ
{{kb_xanderia_faq}}
## PRICING
{{kb_pricing_2025}}
## PROCEDURES
{{kb_procedures}}
Use the above context to answer customer questions accurately.
If the answer is not in the reference material, say you will check and call back.
Outbound — Single Live SIP Call
Places one AI-driven outbound call using a SIP trunk connected to a LiveKit room.
| Field | Required | Description |
|---|---|---|
| destination | required | E.164 phone number or SIP URI to call |
| from | optional | Caller ID to present |
| assistant | required | Full assistant object (or reference by id) |
| row | optional | CSV row object — keys injected as template variables |
| campaignId | optional | Associate call with a campaign |
| campaignName | optional | Campaign label |
{
"destination": "+60123456789",
"from": "+60312345678",
"assistant": {
"id": "asst_abc123",
"name": "Maya",
"voiceProvider": "ytl",
"voice": "voice_1",
"llmProvider": "ytl",
"firstMessage": "Hello {{customer_name}}, this is Maya calling from Xanderia.",
"systemPrompt": "You are Maya...\n{{kb_faq}}"
},
"row": {
"customer_name": "Ahmad Zaki",
"LoanAmount": "15000",
"AccountRef": "XND-2025-0012"
}
}
{
"success": true,
"provider": "livekit-sip",
"roomName": "voxpro-room-1719825600-xnd",
"destination": "+60123456789",
"from": "+60312345678",
"participantIdentity": "sip-participant-abc",
"trunkId": "ST_abc123",
"trunkName": "voxpro-main-trunk",
"callStatus": "dialing"
}
Blast Campaigns
A blast campaign dials a list of numbers concurrently, tracks each job, and can automatically retry unanswered / voicemail calls after a configurable interval.
| Field | Required | Description |
|---|---|---|
| rows | required | Array of CSV row objects. Each must contain the destination phone number. |
| destinationColumn | required | Which CSV column holds the phone number |
| nameColumn | optional | Column for customer display name |
| assistant | required | Full assistant object |
| from | optional | Caller ID |
| concurrency | optional | Simultaneous calls (1–10). Default: 1 |
| maxAttempts | optional | Total call attempts per number (1–10). Default: 1 |
| retryIntervalMinutes | optional | Minutes between retries. Default: 15 |
| campaignName | optional | Label shown in the UI |
| campaignType | optional | e.g. "loan_collection" — free text tag |
{
"campaignName": "June Collections",
"campaignType": "loan_collection",
"destinationColumn": "PhoneNumber",
"nameColumn": "CustomerName",
"from": "+60312345678",
"concurrency": 3,
"maxAttempts": 2,
"retryIntervalMinutes": 20,
"assistant": { "id": "asst_abc123", "..." },
"rows": [
{ "PhoneNumber": "+60123456789", "CustomerName": "Ahmad", "LoanAmount": "5000" },
{ "PhoneNumber": "+60198765432", "CustomerName": "Siti", "LoanAmount": "12000" }
]
}
{
"success": true,
"blast": {
"id": "blast-1719825600-a1b2c3",
"campaignName": "June Collections",
"totalRows": 2,
"queued": 2,
"failed": 0,
"maxAttempts": 2,
"retryIntervalMinutes": 20
},
"results": [
{
"index": 0,
"to": "+60123456789",
"status": "started",
"roomName": "voxpro-room-123-abc"
}
]
}
Returns campaigns with live stats and recent call logs.
{
"campaigns": [
{
"id": "blast-1719825600-a1b2c3",
"campaignName": "June Collections",
"stats": {
"totalTargets": 50,
"called": 48,
"active": 3,
"completed": 32,
"failed": 5,
"pending": 2
}
}
]
}
Returns per-job status, call outcomes, active rooms, and retry state.
Job status values
Retryable endedReason values
Calls with these ended reasons are eligible for automatic retry:
voicemailcustomer-did-not-answercustomer-busysilence-timed-outfailed-to-connectcall.error-processing-failed
Inbound Routing
Map DIDs (Direct Inward Dialing numbers) to assistants. When your SIP provider routes an inbound call to VoxPro, it POSTs to /inbound/call — VoxPro looks up the DID, starts a LiveKit room, and connects the SIP call.
{
"numbers": [
{
"id": "num_abc",
"number": "+60312345678",
"label": "Sales hotline",
"enabled": true,
"assistantId": "asst_abc123",
"assistantName": "Maya"
}
]
}
{
"number": "+60312345678",
"label": "Sales hotline",
"enabled": true,
"assistantId": "asst_abc123"
}
| Field | Description |
|---|---|
| to / toNumber / called | The DID that was dialed (any alias works) |
| from / fromNumber / caller | Caller's number |
| callId | Optional call ID for logging |
{
"ok": true,
"roomName": "voxpro-inbound-room-xyz",
"assistantId": "asst_abc123",
"assistantName": "Maya",
"sipUri": "sip:voxpro-inbound-room-xyz@livekit.example.com"
}
Call Lifecycle
Understanding the sequence of events helps you know when to query logs and what to expect from automations.
dialed
created
first message
→ TTS loop
hangup
automations fire
endedReason values
| Value | Meaning |
|---|---|
| customer-ended-call | Caller hung up normally |
| customer-did-not-answer | No answer within ring timeout |
| customer-busy | Line busy |
| voicemail | Call reached voicemail |
| silence-timed-out | Auto-hang-up after silence threshold |
| failed-to-connect | SIP connection error |
| agent-ended-call | AI agent intentionally ended the call |
| call.error-processing-failed | Internal pipeline error |
Webhook Tools — Overview
Webhook Tools let the AI call your HTTP endpoint during a conversation to look up live data, trigger actions, or transfer the call. The agent emits a special token in its reply; VoxPro intercepts it, calls your URL, and feeds the response back to the LLM before continuing.
Tool types
webhook
POST / GET to your URL with JSON arguments. You return a JSON or plain-text result. Best for CRM lookups, order status, real-time data.
query
VoxPro searches an uploaded CSV knowledge base locally — no external HTTP call needed. Returns matching rows as text.
transferCall
When the agent detects the trigger, the SIP call is transferred to a configured destination number.
Webhook Tools — Building a Tool
How the agent calls a tool
The LLM emits a token in this format. VoxPro intercepts it before sending TTS:
[[tool:your-tool-id {"param1": "value", "param2": 123}]]
VoxPro then POSTs to your webhook URL:
{
"toolCallId": "tc_abc123",
"toolId": "crm-lookup",
"arguments": {
"phone_number": "+60123456789",
"account_ref": "XND-2025-0012"
}
}
Your webhook response
Return any JSON or plain text. VoxPro converts it to a string and injects it as a TOOL_RESULT message to the LLM:
{
"customerName": "Ahmad Zaki",
"loanBalance": 14850.00,
"dueDate": "2025-07-01",
"status": "overdue"
}
Configuring a tool
{
"name": "CRM Lookup",
"toolId": "crm-lookup",
"description": "Look up a customer's loan status by phone number or account ref",
"type": "webhook",
"method": "POST",
"webhookUrl": "https://yourapi.example.com/voxpro/crm-lookup",
"bodySchemaJson": "{\"type\":\"object\",\"properties\":{\"phone_number\":{\"type\":\"string\"},\"account_ref\":{\"type\":\"string\"}}}",
"headersJson": "{\"x-api-key\": \"secret123\"}"
}
toolId to the assistant's selectedToolIds array. Also instruct the agent in the system prompt on when to call the tool.System prompt instruction example
When the customer asks about their loan balance or due date,
call the crm-lookup tool with their phone number.
[[tool:crm-lookup {"phone_number": "their number here"}]]
Webhook Tools — REST API
{
"tools": [
{
"id": "tool_abc",
"toolId": "crm-lookup",
"name": "CRM Lookup",
"type": "webhook",
"method": "POST",
"webhookUrl": "https://yourapi.example.com/crm",
"description": "Look up customer loan status"
}
]
}
{
"toolId": "crm-lookup",
"args": { "phone_number": "+60123456789" }
}
{
"ok": true,
"toolId": "crm-lookup",
"name": "CRM Lookup",
"status": 200,
"responseText": "{\"customerName\":\"Ahmad\",\"balance\":14850}"
}
Automations — Call Event Webhooks
Automations fire after a call ends (or on other events). Configure a webhook URL and select which fields to include — VoxPro POSTs a structured payload to your system automatically.
Supported trigger events
Selectable fields
phoneNumberCaller / dialed numbercontactIdContact identifierroomNameLiveKit room IDtranscriptFull call transcriptreplyLast agent replycallDurationMsCall length in msoutcomeCall outcome / dispositiontransferTargetTransfer destination if escalatedexpectedPaymentDatePTP date if capturedmatchedCsvRowFull matched CSV row objectmatchedCsvRowsAll matching CSV rowscsv.<ColumnName>Individual CSV column valueAutomations — Payload Reference
When an automation fires, VoxPro POSTs this JSON to your webhook URL:
{
"meta": {
"automationId": "auto_abc123",
"automationName": "Post-call to CRM",
"provider": "voxpro",
"triggerEvent": "call.completed",
"occurredAt": "2025-06-20T14:32:00.000Z"
},
"data": {
"phoneNumber": "+60123456789",
"callDurationMs": 185000,
"outcome": "PTP",
"transcript": "Agent: Hello Ahmad...\nCustomer: Yes I can pay..."
},
"assistant": {
"id": "asst_abc123",
"name": "Maya",
"dataset": "June Collections"
},
"resolvedMatchContext": {
"matchedPhoneNumber": "+60123456789",
"matchedNric": null,
"matchedName": "Ahmad Zaki",
"detectedMonthYear": null
},
"matchedCsvRow": {
"PhoneNumber": "+60123456789",
"CustomerName": "Ahmad Zaki",
"LoanAmount": "5000"
},
"csv": {
"PhoneNumber": "+60123456789",
"CustomerName": "Ahmad Zaki",
"LoanAmount": "5000"
}
}
Custom payload
Use customPayload (a JSON string) to restructure the payload for your system. Supports {{field}} interpolation from data:
{
"contact_phone": "{{phoneNumber}}",
"call_result": "{{outcome}}",
"duration_seconds": "{{callDurationMs}}",
"account": "{{csv.AccountRef}}"
}
Automations — REST API
{
"name": "Post-call to CRM",
"triggerEvent": "call.completed",
"enabled": true,
"webhookUrl": "https://yourapp.example.com/webhooks/voxpro",
"selectedFields": ["phoneNumber", "transcript", "outcome", "matchedCsvRow"],
"customPayload": ""
}
Sends a sample payload to the URL without triggering a real call.
{
"webhookUrl": "https://yourapp.example.com/webhooks/voxpro",
"triggerEvent": "call.completed",
"selectedFields": ["phoneNumber", "outcome"],
"sampleData": { "phoneNumber": "+60123456789" }
}
{
"triggerEvent": "call.completed",
"payload": {
"phoneNumber": "+60123456789",
"roomName": "voxpro-room-xyz",
"outcome": "PTP"
}
}
{
"triggerEvent": "call.completed",
"matched": 2,
"results": [
{ "id": "auto_abc", "name": "Post-call CRM", "ok": true, "status": 200 },
{ "id": "auto_xyz", "name": "n8n Pipeline", "ok": true, "status": 200 }
]
}
Call Logs — REST API
{
"logs": [
{
"id": "voxpro-room-1719825600-abc",
"phoneNumber": "+60123456789",
"contactName": "Ahmad Zaki",
"assistantId": "asst_abc123",
"assistantName": "Maya",
"startedAt": "2025-06-20T14:00:00.000Z",
"updatedAt": "2025-06-20T14:03:05.000Z",
"endedReason": "customer-ended-call",
"outcome": "PTP",
"durationSeconds": 185,
"recordingPath": "/recordings/voxpro-room-abc.wav",
"campaignId": "blast-1719825600-a1b2c3",
"campaignName": "June Collections",
"structuredOutput": {
"disposition": "PTP",
"paymentDate": "2025-07-01",
"amountPromised": 5000
}
}
]
}
Returns the full log object including transcript and structured output.
Recordings
Returns audio/wav. Supports HTTP Range requests for seeking. Add ?download=1 to force a file download instead of inline playback.
// Authenticated streaming with fetch + Blob URL
const res = await fetch(`/logs/${logId}/recording`, {
headers: { Authorization: `Bearer ${token}` }
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
audioElement.src = url;
Customer Allocation
Customer allocation controls how many concurrent calls and monthly minutes each client can consume. Enforced at blast campaign launch time.
{
"customerId": "acme-bpo",
"customerName": "ACME BPO",
"status": "active",
"maxConcurrentCalls": 10,
"enforceConcurrencyLimit": true,
"monthlyMinuteLimit": 5000,
"enforceMinuteLimit": true,
"assistantIds": ["asst_abc123"]
}
Query param ?month=2025-06 for a specific month. Defaults to current month.
{
"customerId": "acme-bpo",
"month": "2025-06",
"totalMinutesUsed": 1248,
"estimatedMinutesRemaining": 3752
}
Voices Catalogue
No auth required. Returns voices grouped by TTS provider. Use the id field as the voice value when configuring an assistant.
{
"ytl": [
{ "id": "voice_1", "name": "Voice 1" },
{ "id": "voice_2", "name": "Voice 2" },
{ "id": "voice_3", "name": "Voice 3" }
],
"edge": [
{ "id": "ms-MY-YasminNeural", "name": "Yasmin — Malay Female" },
{ "id": "ms-MY-OsmanNeural", "name": "Osman — Malay Male" },
{ "id": "en-SG-LunaNeural", "name": "Luna — English SG Female" }
],
"openai": [
{ "id": "alloy", "name": "Alloy" },
{ "id": "shimmer", "name": "Shimmer" }
]
}
voice_1, voice_2, or voice_3. Do not mix providers — if voiceProvider is ytl, the voice must be one of those three IDs.