The Syncli Voice Agent is now available as an external API, similar to ChatGPT or Grok. You can interact with it via standard HTTP endpoints for text chat completion, voice WebRTC sessions, and custom tool integrations via the Model Context Protocol (MCP).
The API runs on the same host as the voice gateway. Use the base URL of the gateway you're connected to:
https://apivoice.syncli.cloud # Production (Docker)
https://apidev.syncli.cloud # Development (host :7445)
https://voice.syncli.cloud # Production alias
device.local:7445 # Local development
All endpoints documented below are relative to this base URL. The frontend auto-detects the base URL from window.location.origin.
All API requests require a Bearer token in the Authorization header.
Authorization: Bearer <your-token>
| Type | Format | Description |
|---|---|---|
| Session JWT | eyJhbG... |
Standard OpenWebUI session token (from browser login) |
| API Key | syncli_... |
Persistent developer key generated via the Developer Portal |
| Legacy Key | sk-... |
Legacy API keys (deprecated, still supported) |
Replace <your-token> with either your session JWT or a syncli_... API key. All three token types are accepted on every endpoint.
chat, voice, mcp).| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/api-keys |
List your active keys |
POST |
/api/v1/api-keys |
Generate a new key |
DELETE |
/api/v1/api-keys/{id} |
Revoke a key |
POST |
/api/v1/api-keys/{id}/rotate |
Rotate (revoke + regenerate) |
POST /v1/chat/completionsStandard OpenAI-compatible chat completion endpoint.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request Body:
{
"model": "syncli-voice",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is on my calendar today?" }
],
"stream": false,
"temperature": 0.1,
"max_tokens": 300,
"conversation_id": "my-session-123"
}
Response (non-streaming):
{
"id": "chatcmpl-syncli",
"object": "chat.completion",
"created": 1779271807,
"model": "syncli-voice",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "You have a meeting with Willy at 2 PM."
},
"finish_reason": "stop"
}
]
}
Streaming: Set "stream": true. The server returns text/event-stream chunks:
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" world"}}]}
data: [DONE]
GET /v1/modelsList available models.
{
"object": "list",
"data": [
{ "id": "qwen2.5:14b-instruct-q4_K_M", "object": "model", "owned_by": "syncli" }
]
}
The agent supports the same tools as the voice bot:
| Tool | Description |
|---|---|
get_calendar_summary |
List upcoming Google Calendar events |
create_google_meet |
Schedule a Google Meet with auto-invite |
add_reminder_task |
Add a task to Google Tasks |
send_email |
Send email via Gmail |
list_contacts |
List Google contacts |
search_contacts |
Search contacts by name/email |
update_user_profile |
Update user learning profile |
Tools are automatically available when you call /v1/chat/completions. The LLM decides which tool to call based on the conversation context.
MCP allows you to connect external tool servers to extend the agent's capabilities.
curl -X POST https://api.syncli.cloud/v1/mcp/servers \
-H "Authorization: Bearer syncli_your_key" \
-H "Content-Type: application/json" \
-d '{
"server_id": "github-tools",
"url": "https://your-mcp-server.com/sse",
"headers": { "Authorization": "Bearer xxxxx" }
}'
Response:
{
"status": "connected",
"server_id": "github-tools",
"tools": ["github_search_repo", "github_create_issue"]
}
| Method | Endpoint | Description |
|---|---|---|
GET |
/v1/mcp/servers |
List registered servers |
POST |
/v1/mcp/servers |
Connect a new server |
DELETE |
/v1/mcp/servers/{id} |
Disconnect a server |
GET |
/v1/mcp/tools |
List all available MCP tools |
Once connected, MCP tools are automatically included in every /v1/chat/completions call.
The voice endpoint uses WebRTC for real-time audio streaming. The connection flow is:
POST /start — Create a session and get ICE configurationRTCPeerConnection with the ICE configPOST /sessions/{sessionId}/api/offer — Send your SDP offerPATCH /sessions/{sessionId}/api/offer
When calling voice endpoints with a syncli_... API key, your frontend must send a stable
end_user_session_id on POST /start (and again in the WebRTC offer request_data).
Without it, the gateway identifies callers by IP only — visitors on the same network share history, rate limits, and webhook identity.
function getBrowserSessionId() {
const key = 'syncli_browser_session_id';
let id = localStorage.getItem(key);
if (!id) {
id = (crypto.randomUUID && crypto.randomUUID())
|| ('bs_' + Date.now() + '_' + Math.random().toString(36).slice(2));
localStorage.setItem(key, id);
}
return id;
}
| Field | Required | Description |
|---|---|---|
end_user_session_id | Yes (API embeds) | Stable per-browser or per-user ID from your frontend |
chat_id | No | Reuse from prior /start response to resume conversation history |
end_user_email | No | When your rate-capping tracking mode includes email |
const token = 'syncli_your_api_key';
function getBrowserSessionId() {
const key = 'syncli_browser_session_id';
let id = localStorage.getItem(key);
if (!id) {
id = (crypto.randomUUID && crypto.randomUUID())
|| ('bs_' + Date.now() + '_' + Math.random().toString(36).slice(2));
localStorage.setItem(key, id);
}
return id;
}
// 1. Start session
const startRes = await fetch('https://apivoice.syncli.cloud/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
end_user_session_id: getBrowserSessionId(),
chat_id: localStorage.getItem('syncli_voice_chat_id') || null,
end_user_email: null,
client_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
client_timestamp: new Date().toISOString()
})
});
const { sessionId, iceServers, chat_id } = await startRes.json();
if (chat_id) localStorage.setItem('syncli_voice_chat_id', chat_id);
// 2. Create peer connection
const pc = new RTCPeerConnection({
iceServers: iceServers,
iceTransportPolicy: 'relay'
});
// 3. Add audio track
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getAudioTracks().forEach(track => pc.addTrack(track, stream));
// 4. Create data channel
dc = pc.createDataChannel('chat');
dc.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('Bot says:', msg.data?.text);
};
// 5. Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const offerRes = await fetch(`https://apivoice.syncli.cloud/sessions/${sessionId}/api/offer`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
sdp: pc.localDescription.sdp,
type: pc.localDescription.type,
request_data: {
end_user_session_id: getBrowserSessionId(),
chat_id: chat_id,
client_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
client_timestamp: new Date().toISOString()
}
})
});
const answer = await offerRes.json();
await pc.setRemoteDescription(new RTCSessionDescription(answer));
// 6. Handle ICE candidates
pc.onicecandidate = async (event) => {
if (!event.candidate) return;
await fetch(`https://apivoice.syncli.cloud/sessions/${sessionId}/api/offer`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
pc_id: answer.pc_id,
candidates: [{
candidate: event.candidate.candidate,
sdp_mid: event.candidate.sdpMid,
sdp_mline_index: event.candidate.sdpMLineIndex
}]
})
});
};
Messages received on the chat data channel:
| Type | Description |
|---|---|
bot-output |
Live assistant text (TTS output) |
bot-output-raw |
Final condensed assistant text |
user-transcript |
What the STT heard |
bot-started-speaking |
Turn start marker |
bot-stopped-speaking |
Turn end marker |
| Code | Meaning |
|---|---|
400 |
Bad request (e.g., malformed JSON) |
401 |
Unauthorized (invalid or missing token) |
429 |
Rate limit exceeded |
500 |
Internal server error |
502 |
MCP server unreachable |
chat, voice, and/or mcp.