# Changelog Source: https://doc.voicedock.ai/docs/changelog Notable, user-facing changes to the VoiceDock platform and API, newest first. Dates reflect public availability. ## July 2026 [#july-2026] * **Grok Voice Think Fast 2.0 is available for xAI Realtime assistants.** xAI's newest speech-to-speech model can now be selected on any assistant using xAI Realtime. It is backwards compatible with the 1.0 settings, so your voice and turn-taking configuration carries over unchanged and only the model name differs. It is **not** the default, and existing assistants keep running 1.0 until you change them yourself. The reason is price: 2.0 costs $0.08 per minute against $0.05 for 1.0, and because provider usage is passed through at cost, that difference lands on your invoice. xAI reports improvements in reasoning, transcription accuracy and time to first audio; those are their published figures, not our measurements. One thing worth knowing if you use the rolling `grok-voice-latest` alias: xAI moves it from 1.0 to 2.0 on 5 August 2026, which changes both the model and the per-minute price without any action on your side. Pin an explicit model name if you would rather decide that moment yourself. See [xAI Grok integration](/docs/integrations/xai-grok-integration). * **Speech recognition moved to Deepgram's EU endpoint, and out of model training.** All Deepgram speech-to-text now runs against `api.eu.deepgram.com` with the Model Improvement Program switched off, so call audio stays in the EU and is never used to train their models or shared with third parties for benchmarking. This is a platform default, deliberately not configurable per assistant: an account should not be able to move its callers outside the EU or into a training set by accident. Nothing to change on your side, and no price difference. See [Privacy and compliance](/docs/features/privacy-compliance). * **Retention windows are now enforced automatically.** Your configured retention period is applied every night: transcripts, summaries and analyses are stripped and recordings are deleted from storage once a call passes the window, with phone numbers and call events following a separate, longer window. Until now the setting described an intention; it is now the mechanism. Existing calls are covered as well. See [Privacy and compliance](/docs/features/privacy-compliance). * **Turn-taking settings for xAI Grok Realtime.** Three fields on the assistant let you tune when Grok decides the caller has stopped speaking: silence before end of turn, speech threshold, and lead-in audio. Leave a field empty and the provider default applies, so existing assistants behave exactly as before. Configure it under Language Model in the dashboard, or set `llm_config.turn_detection` through the API. See [xAI Grok integration](/docs/integrations/xai-grok-integration). * **Interruption detection now runs on our own infrastructure.** Deciding whether a sound is a real interruption or just a listener saying "mm-hm" previously called a hosted service on every interruption, which added an external dependency to the live call path and carried a request ceiling. That work now happens locally. No configuration and no behaviour change to tune; it removes a moving part from the path a call depends on. * **Fix: telephony lines on your own SIP trunk no longer show a carrier.** Calls on a bring-your-own trunk listed our carrier and its tariff class in the cost breakdown, even though the leg was never priced by us and never billed. Those lines now correctly show no provider and no tariff class. Managed numbers are unchanged. See [Billing](/docs/platform/billing#telephony-costs). * **Fix: parts of the dashboard showed Dutch text in the English interface.** The advanced turn-taking panels for xAI Grok Realtime and Gemini Live were not translated. * **Automatic failover — our first stable release (1.0).** VoiceDock now runs active-passive: a warm standby mirrors production continuously and takes over automatically if the primary stops responding, typically within about 90 seconds and with nothing to change on your side. We verified it end to end on production with a controlled outage — a real inbound call was answered on the standby with recording, analysis and billing intact. If a failover ever happens, we are paged automatically. * **Sign in to the MCP server with your account (OAuth 2.1).** Connect Claude, Claude Code, Cursor, or any MCP client to the [MCP server](/docs/integrations/mcp-server) with just the URL — no API key to copy. Your client opens a browser, you log in with your VoiceDock account and approve access on a consent screen, and it works against your organization. A raw API key still works as a bearer token for CI and scripts. * **Multiple recipients and action buttons for end-of-call reports.** Send the post-call report email to up to five addresses per assistant, and choose which buttons the email shows — an "Open in dashboard" link, a direct "Listen to recording" button, both, or neither. Configure it on the assistant in the dashboard. See [Call analysis](/docs/features/call-analysis). * **Itemized telephony costs on managed numbers.** Calls on platform-managed phone numbers now meter the phone legs as a provider cost at carrier list rates, itemized per call in the cost breakdown next to model usage, on top of the unchanged €0,07/min orchestration fee. A transferred call shows the inbound line and the outbound leg to the destination as separate lines; unanswered transfer attempts cost nothing. On a transferred call the per-minute rate stops at the moment the transfer connects, so the human-to-human part of the conversation carries telephony cost only, with no model usage and no orchestration fee. Numbers on your own SIP trunk are exempt, exactly like BYOK for models. See [Billing](/docs/platform/billing#telephony-costs). * **No-answer handling for call transfers.** Transfers can now wait for the destination to actually answer before connecting the caller (`wait_for_answer` on the transfer tool). If nobody picks up within the configurable timeout — or the line is busy — the assistant stays with the caller and can take a message, try one of the configurable backup numbers (`fallback_destinations`, tried in order), or end the call politely. Existing transfers are unchanged: without the flag, transfers connect immediately as before. See [Call Transfers](/docs/features/call-transfers#no-answer-handling-monitored-transfers). * **Workflows (Beta).** Build multi-step call flows on a visual canvas: conversation steps with their own instructions — and optionally their own model, voice, text-to-speech or speech-recognition settings — background tool calls that always run, guaranteed human transfers and clean endings. Global steps such as "back to reception" are reachable from anywhere without drawing lines. One assistant keeps supplying the defaults and call settings; the workflow drives inbound calls on the numbers it is attached to. Attach a workflow to a phone number in the dashboard or via the API. See [Workflows](/docs/features/workflows) and the [Workflows API](/docs/api/workflows/listWorkflows). * **Webhooks per phone number.** A webhook can now be set directly on a phone number, alongside per-assistant and account-wide webhooks. For each event, VoiceDock delivers to exactly one endpoint, with precedence assistant → phone number → account: a number's webhook is used when its assigned assistant has none, and takes precedence over the account webhook. See [Webhooks overview](/docs/webhooks/overview). ## June 2026 [#june-2026] * **Developer logs.** A new **Logs** page in the dashboard shows what happens on each call — when it starts and ends, and the errors that stop a call, such as an unknown model or an invalid tool definition. Scoped to your own organization, so you can debug an assistant without opening a support ticket. * **More natural call endings.** Assistants now finish their closing line cleanly before hanging up, with no mid-sentence cut-off or trailing silence, including on realtime speech-to-speech models. * **End-of-call reports for platform-key assistants.** Assistants running on platform-provided keys (without BYOK) now also receive a post-call summary and structured analysis, just like BYOK assistants. See [Call analysis](/docs/features/call-analysis). * **Usage-based, at-cost pricing.** You pay for actual model usage at cost, plus a flat €0.07 per minute orchestration fee. Vertex AI Live is a flat €0.25 per minute, all-in. See [Billing](/docs/platform/billing). * **Bring-your-own-key (BYOK) is now optional.** Assistants work out of the box on platform-provided keys; add your own provider keys only if you want to. See [BYOK setup](/docs/integrations/byok-setup). ## May 2026 [#may-2026] * **Documentation and full API reference launched** — complete guides plus an interactive REST API reference. Start at the [Quickstart](/docs/get-started/quickstart). * **xAI Grok for realtime voice** — low-latency realtime speech, plus a Grok text-to-speech option. See [xAI Grok integration](/docs/integrations/xai-grok-integration). * **Branded end-of-call report emails** — per-organization branding, an "Open in dashboard" link, and a recording hint. * **Configurable call duration up to 30 minutes** per assistant (`max_duration_seconds`). See [Assistants](/docs/core-concepts/assistants). ## April 2026 [#april-2026] * **Google Gemini Live** added as a realtime speech-to-speech provider, including a **Vertex AI** option. See [Provider pricing](/docs/integrations/provider-pricing). * **Google Gemini text-to-speech** added as a provider. * **Email notifications for web calls.** ## March 2026 [#march-2026] * **Official Node.js / TypeScript SDK** released — install `hmsovereign` from npm. See [Node SDK](/docs/sdks/node). * **MCP server** for the platform, hosted at `mcp.hmsovereign.com`. See [MCP server](/docs/integrations/mcp-server). * **Recording consent flow (DTMF)** — callers can be asked to press 1 to consent before any processing begins. See [Privacy & compliance](/docs/features/privacy-compliance). * **More providers** — Mistral (Voxtral) speech-to-text and Inworld text-to-speech. * **Improved multilingual turn detection and interruption handling.** ## February 2026 [#february-2026] * **More provider options** — Mistral and xAI Grok as text models, Gladia and ElevenLabs Scribe as speech-to-text. * **GDPR mode** for per-assistant data-retention control. See [Privacy & compliance](/docs/features/privacy-compliance). * **Configurable silence timeout** with a faster default. ## January 2026 [#january-2026] * **Web calls** — browser-based WebRTC calls, with a public embeddable web-calls API and whitelabel support. See [Web calls](/docs/features/web-calls). * **Call recording** with signed URLs for secure access. * **Voicemail detection** and a configurable voicemail message. See [Voicemail detection](/docs/features/voicemail-detection). * **Autonomous silence handling** — recurring prompts when a caller goes quiet. See [Autonomous silence handling](/docs/features/autonomous-silence-handling). * **Outbound campaigns** — campaign tracking for outbound calls. See [Campaigns](/docs/features/campaigns). * **Assistants can speak while running a tool**, with async tool results fed back into the conversation. See [Custom tools](/docs/configuration/custom-tools). * **Richer webhook events** — deterministic end reasons, call timestamps, and phone-number and direction fields. * **Real-time sync webhook API**, replacing polling. * **Free local voices** for text-to-speech. * **Prompt template variables** such as `{{ now }}`. ## December 2025 [#december-2025] * **Public API foundations** — assistants, calls, and phone numbers as first-class resources, with agent configuration separated from phone numbers. * **Webhooks** — assistant-request (pre-call config override), status-update, tool-calls (function calling), and end-of-call-report with full transcript. See [Webhooks](/docs/webhooks/overview). * **Live Call Control API** — inject context, speak, transfer, or end a call mid-conversation. * **Built-in call control** — LLM-controlled `end_call` and call transfer. See [Call transfers](/docs/features/call-transfers). * **Outbound call API.** * **Post-call structured analysis.** See [Call analysis](/docs/features/call-analysis). * **Bring-your-own-key (BYOK) providers** and **SIP trunk** support. See [SIP trunks](/docs/configuration/sip-trunks). * **xAI Grok** realtime speech-to-speech provider. * **Whitelabel** support — child organizations, per-organization email domains, and branded summaries. See [Whitelabel](/docs/platform/whitelabel). * **Usage-based billing** in credits, at a flat €0.07 per minute. * **Multilingual** emails and call summaries. # Introduction Source: https://doc.voicedock.ai/docs/introduction **VoiceDock** is a voice AI platform built and operated by **Flireo B.V.**, a Dutch company. It runs on **HMS Sovereign**, our own orchestration engine. Build AI voice assistants that understand natural language, handle inbound and outbound phone calls, run custom functions over webhooks, and connect to the systems you already use, all through one REST API. > VoiceDock is the product, Flireo B.V. is the company behind it, and HMS Sovereign is the technology that powers it. See [VoiceDock, Flireo & HMS Sovereign](/docs/platform/about) for how the three relate. ## Start here [#start-here] ## What you can build [#what-you-can-build] * **AI voice assistants** — assistants with custom instructions, voices, and behaviour. * **Inbound and outbound calls** — answer calls or place them programmatically, with natural conversation flow. * **Custom functions** — let assistants take action mid-call through your webhooks and tools. * **Call analysis** — get structured insights and scoring from every transcript. * **Integrations everywhere** — connect to your stack over the REST API and webhooks. ## Authentication [#authentication] Every request authenticates with an API key in the `Authorization` header. You can find and manage your key in the [dashboard](https://dashboard.hmsovereign.com). ```bash curl https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript const response = await fetch("https://api.hmsovereign.com/api/v1/assistants", { headers: { Authorization: "Bearer YOUR_API_KEY" }, }) ``` ```python import requests response = requests.get( "https://api.hmsovereign.com/api/v1/assistants", headers={"Authorization": "Bearer YOUR_API_KEY"}, ) ``` Keep your API key secret. Never expose it in client-side code or commit it to source control. ## Next steps [#next-steps] Ready to build? Start with the [Quickstart](/docs/get-started/quickstart), or jump straight into the [API Reference](/docs/api/assistants/listAssistants). # Analysis Templates Source: https://doc.voicedock.ai/docs/configuration/analysis-templates Analysis templates let you define reusable configurations for post-call analysis. Instead of configuring analysis on each assistant, create templates and reference them across multiple assistants. ## What are Analysis Templates? [#what-are-analysis-templates] An analysis template defines: * **Schema** - The structured data format to extract * **Prompts** - Instructions for the AI analyzer * **Settings** - Minimum messages threshold and other options ## Creating an Analysis Template [#creating-an-analysis-template] ```bash curl -X POST https://api.hmsovereign.com/api/v1/analysis-templates \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer Satisfaction Analysis", "description": "Extract satisfaction metrics from support calls", "schema": { "type": "object", "properties": { "satisfaction_score": { "type": "integer", "minimum": 1, "maximum": 10, "description": "Overall customer satisfaction (1=very dissatisfied, 10=very satisfied)" }, "issue_resolved": { "type": "boolean", "description": "Was the customer issue fully resolved?" }, "sentiment": { "type": "string", "enum": ["positive", "neutral", "negative"] }, "topics": { "type": "array", "items": { "type": "string" }, "description": "Main topics discussed" }, "follow_up_needed": { "type": "boolean" } } }, "messages": [ { "role": "system", "content": "Analyze the customer support call transcript and extract structured data according to the schema. Be objective and base your analysis on the actual conversation content." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript:\n{{transcript}}\n\nCall ended because: {{ended_reason}}\n\nProvide your analysis as valid JSON." } ], "min_messages_threshold": 5 }' ``` **Response:** ```json { "analysis_template": { "id": "template-uuid", "name": "Customer Satisfaction Analysis", "description": "Extract satisfaction metrics from support calls", "schema": { ... }, "messages": [ ... ], "min_messages_threshold": 5, "created_at": "2025-12-13T10:00:00.000Z" } } ``` ## Template Fields [#template-fields] | Field | Type | Required | Description | | ------------------------ | ------- | -------- | ----------------------------------------------------- | | `name` | string | Yes | Display name | | `description` | string | No | Description of what this template analyzes | | `schema` | object | Yes | JSON Schema for output structure | | `messages` | array | Yes | Prompt messages with placeholders | | `min_messages_threshold` | integer | No | Minimum messages before running analysis (default: 2) | ## Using Templates with Assistants [#using-templates-with-assistants] Reference a template when creating or updating an assistant: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "analysis_template_id": "template-uuid" }' ``` The template's configuration will be used for all calls handled by this assistant. ## Template Examples [#template-examples] ### Sales Lead Qualification [#sales-lead-qualification] ```json { "name": "Sales Lead Qualification", "schema": { "type": "object", "properties": { "lead_quality": { "type": "string", "enum": ["hot", "warm", "cold"] }, "budget_discussed": { "type": "boolean" }, "timeline": { "type": "string", "description": "When does the prospect want to buy?" }, "decision_maker": { "type": "boolean", "description": "Is the caller a decision maker?" }, "products_interested": { "type": "array", "items": { "type": "string" } }, "next_steps": { "type": "string" } } }, "messages": [ { "role": "system", "content": "Analyze this sales call and qualify the lead based on BANT criteria (Budget, Authority, Need, Timeline)." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript: {{transcript}}" } ] } ``` ### Support Ticket Classification [#support-ticket-classification] ```json { "name": "Support Ticket Classification", "schema": { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "technical", "account", "product", "general"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "resolution_status": { "type": "string", "enum": ["resolved", "escalated", "pending", "unresolved"] }, "root_cause": { "type": "string" }, "action_items": { "type": "array", "items": { "type": "string" } } } }, "messages": [ { "role": "system", "content": "Classify this support call for ticket creation. Identify the category, priority, and resolution status." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript: {{transcript}}\n\nCall ended: {{ended_reason}}" } ] } ``` ### Appointment Booking [#appointment-booking] ```json { "name": "Appointment Booking Analysis", "schema": { "type": "object", "properties": { "appointment_booked": { "type": "boolean" }, "appointment_date": { "type": "string", "description": "Date in YYYY-MM-DD format if booked" }, "appointment_time": { "type": "string", "description": "Time in HH:MM format if booked" }, "service_type": { "type": "string" }, "customer_name": { "type": "string" }, "notes": { "type": "string" } } }, "messages": [ { "role": "system", "content": "Extract appointment booking details from this call. If no appointment was booked, set appointment_booked to false." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript: {{transcript}}" } ] } ``` ## Listing Templates [#listing-templates] ```bash curl https://api.hmsovereign.com/api/v1/analysis-templates \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Updating a Template [#updating-a-template] ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/analysis-templates/TEMPLATE_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "min_messages_threshold": 3 }' ``` ## Deleting a Template [#deleting-a-template] ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/analysis-templates/TEMPLATE_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` > **Warning**: Deleting a template does not affect assistants using it. They will continue to use the configuration they had at the time of association. *** ## Related [#related] * [Call Analysis Guide](../features/call-analysis.md) * [Analysis Templates API](/docs/api/analysis-templates/listAnalysisTemplates) * [End of Call Report Webhook](/docs/webhooks/end-of-call-report) # Custom Tools Source: https://doc.voicedock.ai/docs/configuration/custom-tools Custom tools allow your AI assistant to execute functions during calls, like looking up customer information, booking appointments, or checking inventory. ## How Tools Work [#how-tools-work] 1. You define tools in your assistant's `llm_config.tools[]` 2. During a call, the LLM decides when to use a tool 3. HMS Sovereign sends a webhook request with the tool call details 4. Your endpoint returns the result 5. The LLM uses the result to continue the conversation ## Defining a Tool [#defining-a-tool] Add tools when creating or updating your assistant: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "tools": [ { "name": "lookup_customer", "description": "Look up customer information by phone number or name. Use this when the caller asks about their account or you need to verify their identity.", "parameters": { "type": "object", "properties": { "phone": { "type": "string", "description": "Customer phone number in E.164 format" }, "name": { "type": "string", "description": "Customer name to search for" } } } }, { "name": "book_appointment", "description": "Book an appointment for the customer. Ask for their preferred date and time first.", "parameters": { "type": "object", "properties": { "date": { "type": "string", "description": "Appointment date in YYYY-MM-DD format" }, "time": { "type": "string", "description": "Appointment time in HH:MM format" }, "service": { "type": "string", "description": "Type of service requested" } }, "required": ["date", "time"] } } ] } }' ``` ## Tool Definition Schema [#tool-definition-schema] | Field | Type | Required | Description | | ---------------- | ------- | -------- | --------------------------------------------------------- | | `name` | string | Yes | Unique tool name (used by LLM) | | `description` | string | Yes | What the tool does (helps LLM decide when to use it) | | `url` | string | No | Per-tool webhook URL (overrides assistant's webhook\_url) | | `async` | boolean | No | Fire-and-forget mode (default: false) | | `async_response` | string | No | Response for async tools | | `parameters` | object | No | JSON Schema for tool inputs | ## Writing Good Descriptions [#writing-good-descriptions] The description is crucial - it tells the LLM when and how to use the tool. **Good descriptions:** ```json { "name": "check_inventory", "description": "Check product availability and stock levels. Use when customer asks if a product is in stock or available. Returns quantity available and location." } ``` ```json { "name": "create_support_ticket", "description": "Create a support ticket for issues that cannot be resolved during the call. Use only when you cannot help the customer directly. Requires a description of the issue." } ``` **Bad descriptions:** ```json { "name": "lookup", "description": "Looks up data" } ``` ## Handling Tool Calls [#handling-tool-calls] When a tool is called, your webhook receives: ```json { "message": { "type": "tool-calls", "tool_call_list": [ { "id": "tool_abc123", "type": "function", "function": { "name": "lookup_customer", "arguments": { "phone": "+31612345678" } } } ] } } ``` Return the result: ```json { "results": [ { "tool_call_id": "tool_abc123", "result": { "found": true, "name": "Jan de Vries", "email": "jan@example.com", "account_type": "premium", "member_since": "2020-03-15" } } ] } ``` ## Per-Tool URLs [#per-tool-urls] Route different tools to different endpoints: ```json { "tools": [ { "name": "lookup_customer", "description": "...", "url": "https://crm.example.com/api/lookup" }, { "name": "book_appointment", "description": "...", "url": "https://calendar.example.com/api/book" } ] } ``` ## Async Tools [#async-tools] For tools that don't need to return a result (logging, notifications): ```json { "name": "log_interest", "description": "Log that the customer expressed interest in a product for follow-up", "async": true, "async_response": "Interest noted for follow-up.", "parameters": { "type": "object", "properties": { "product": { "type": "string" }, "notes": { "type": "string" } } } } ``` The LLM immediately receives "Interest noted for follow-up." while your webhook processes in the background. ## Built-in Tools [#built-in-tools] ### End Call [#end-call] Allow the assistant to end the call: ```json { "type": "end_call" } ``` ### Transfer Call [#transfer-call] Allow the assistant to transfer to specific destinations: ```json { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Sales team - for pricing questions and quotes", "message": "I'm transferring you to our sales team." }, { "type": "number", "number": "+31687654321", "description": "Technical support - for product issues", "message": "I'm transferring you to technical support." } ] } ``` ## Complete Example [#complete-example] Assistant configuration with multiple tools: ```json { "name": "Customer Service Assistant", "webhook_url": "https://api.example.com/webhooks/hms-sovereign", "webhook_secret": "secret123", "webhook_events": ["tool-calls", "end-of-call-report"], "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "messages": [ { "role": "system", "content": "You are a customer service agent. Use the available tools to help customers." } ], "tools": [ { "name": "lookup_customer", "description": "Look up customer by phone number", "parameters": { "type": "object", "properties": { "phone": { "type": "string" } }, "required": ["phone"] } }, { "name": "check_order_status", "description": "Check the status of a customer order", "parameters": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] } }, { "type": "end_call" }, { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Human support for complex issues" } ] } ] } } ``` See [Tool Calls Webhook](/docs/webhooks/tool-calls) for webhook details. # SIP Trunks Source: https://doc.voicedock.ai/docs/configuration/sip-trunks SIP trunks enable call transfers from your AI assistant to external phone systems, such as your office PBX or contact center. ## What is a SIP Trunk? [#what-is-a-sip-trunk] A SIP trunk is a connection to a phone system that allows calls to be transferred over the internet using the SIP (Session Initiation Protocol) protocol. ## When Do You Need a SIP Trunk? [#when-do-you-need-a-sip-trunk] * **Call transfers to your PBX** - Route calls to internal extensions * **Contact center integration** - Transfer to human agents in your call center * **Multi-location routing** - Route calls to different offices ## Creating a SIP Trunk [#creating-a-sip-trunk] ```bash curl -X POST https://api.hmsovereign.com/api/v1/sip-trunks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Office PBX", "host": "pbx.example.com", "port": 5060, "username": "hms-sovereign", "password": "secure-password" }' ``` **Response:** ```json { "sip_trunk": { "id": "trunk-uuid", "name": "Office PBX", "host": "pbx.example.com", "port": 5060, "username": "hms-sovereign", "created_at": "2025-12-13T10:00:00.000Z" } } ``` ### SIP Trunk Fields [#sip-trunk-fields] | Field | Type | Required | Description | | ---------- | ------- | -------- | -------------------------- | | `name` | string | Yes | Display name for the trunk | | `host` | string | Yes | SIP server hostname or IP | | `port` | integer | No | SIP port (default: 5060) | | `username` | string | No | Authentication username | | `password` | string | No | Authentication password | > **Note**: Passwords are stored securely and encrypted at rest. They are never exposed in API responses. ## Assigning a Trunk to a Phone Number [#assigning-a-trunk-to-a-phone-number] After creating a trunk, assign it to phone numbers that should use it for transfers: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "transfer_trunk_id": "trunk-uuid" }' ``` Now transfers from this phone number will route through your SIP trunk. ## Using Transfers [#using-transfers] ### Assistant-Initiated Transfers [#assistant-initiated-transfers] Add the transfer tool to your assistant: ```json { "llm_config": { "tools": [ { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Human support assistant", "message": "I'm transferring you to a colleague." } ] } ] } } ``` ### API-Initiated Transfers [#api-initiated-transfers] Transfer an active call via the API: ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "transfer", "destination": "+31612345678", "message": "One moment, I'm transferring you." }' ``` ## SIP Trunk Configuration Examples [#sip-trunk-configuration-examples] ### FreePBX / Asterisk [#freepbx--asterisk] ```json { "name": "FreePBX", "host": "192.168.1.100", "port": 5060, "username": "hms-sovereign-trunk", "password": "your-trunk-password" } ``` FreePBX configuration: 1. Create a new SIP trunk in **Connectivity > Trunks** 2. Set the trunk name and peer details 3. Add HMS Sovereign's IP to your firewall allowlist ### 3CX [#3cx] ```json { "name": "3CX Office", "host": "yourcompany.3cx.com", "port": 5060, "username": "hms-sovereign", "password": "trunk-password" } ``` ### Generic SIP Provider [#generic-sip-provider] ```json { "name": "SIP Provider", "host": "sip.provider.com", "port": 5060, "username": "your-account", "password": "your-password" } ``` ## Listing SIP Trunks [#listing-sip-trunks] ```bash curl https://api.hmsovereign.com/api/v1/sip-trunks \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Deleting a SIP Trunk [#deleting-a-sip-trunk] When you delete a SIP trunk, any phone numbers using it will have their `transfer_trunk_id` set to `null`: ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/sip-trunks/TRUNK_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Troubleshooting [#troubleshooting] ### Transfers Not Connecting [#transfers-not-connecting] 1. Verify the SIP trunk host is accessible 2. Check firewall allows SIP traffic (port 5060) 3. Verify credentials are correct 4. Test the destination number directly ### Audio Issues After Transfer [#audio-issues-after-transfer] 1. Check NAT configuration on your PBX 2. Verify RTP ports are open 3. Ensure codecs are compatible *** ## Related [#related] * [Call Transfers Guide](../features/call-transfers.md) * [SIP Trunks API](/docs/api/sip-trunks/listSipTrunks) * [Phone Numbers API](/docs/api/numbers/listNumbers) # Tool Templates Source: https://doc.voicedock.ai/docs/configuration/tool-templates Tool templates let you define reusable tool configurations that can be easily added to multiple assistants. Instead of copying tool definitions, create templates and reference them when configuring assistants. ## What are Tool Templates? [#what-are-tool-templates] A tool template defines: * **Tool type** - Function, end call, or transfer * **Name and description** - For LLM understanding * **Parameters** - JSON Schema for inputs * **Configuration** - URLs, async settings, etc. ## Creating a Tool Template [#creating-a-tool-template] ### Function Tool Template [#function-tool-template] ```bash curl -X POST https://api.hmsovereign.com/api/v1/tool-templates \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "lookup_customer", "description": "Look up customer information by phone number or email. Use when you need to verify customer identity or retrieve their account details.", "type": "function", "url": "https://api.example.com/webhooks/lookup", "async": false, "parameters": { "type": "object", "properties": { "phone": { "type": "string", "description": "Customer phone number in E.164 format" }, "email": { "type": "string", "description": "Customer email address" } } } }' ``` **Response:** ```json { "tool_template": { "id": "template-uuid", "name": "lookup_customer", "description": "Look up customer information...", "type": "function", "url": "https://api.example.com/webhooks/lookup", "async": false, "parameters": { ... }, "created_at": "2025-12-13T10:00:00.000Z" } } ``` ### End Call Tool Template [#end-call-tool-template] ```bash curl -X POST https://api.hmsovereign.com/api/v1/tool-templates \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "end_call", "description": "End the call when the conversation is complete", "type": "end_call" }' ``` ### Transfer Call Tool Template [#transfer-call-tool-template] ```bash curl -X POST https://api.hmsovereign.com/api/v1/tool-templates \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "transfer_to_support", "description": "Transfer to human support team", "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Human support assistant for complex issues", "message": "I'm transferring you to a colleague, one moment please." } ] }' ``` ## Template Fields [#template-fields] | Field | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------ | | `name` | string | Yes | Tool name (used by LLM) | | `description` | string | Yes | What the tool does | | `type` | string | Yes | `function`, `end_call`, or `transfer_call` | | `url` | string | No | Webhook URL for function tools | | `async` | boolean | No | Fire-and-forget mode (default: false) | | `async_response` | string | No | Response for async tools | | `parameters` | object | No | JSON Schema for inputs | | `destinations` | array | No | Transfer destinations (for transfer\_call) | ## Using Templates with Assistants [#using-templates-with-assistants] Templates are used by adding tools to an assistant's `llm_config.tools` array. Each tool that originates from a template includes a `_template_id` field that links it back to the template. When you update a template, **all assistants that use it are automatically synced** — the tool definition is updated in every agent's `llm_config.tools`. To add a template-based tool to an assistant, include it in `llm_config.tools` with the `_template_id` field: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "llm_config": { "tools": [ { "_template_id": "lookup-template-uuid", "name": "lookup_customer", "description": "Look up customer information by phone number or email.", "parameters": { "type": "object", "properties": { "phone": { "type": "string" } } }, "url": "https://api.example.com/webhooks/lookup" } ] } }' ``` The easiest way to add templates to an assistant is via the dashboard — select templates directly in the assistant editor and they are added automatically with the correct `_template_id` reference. ## Template Examples [#template-examples] ### Check Appointment Availability [#check-appointment-availability] ```json { "name": "check_availability", "description": "Check available appointment slots for a given date and service type. Use when customer wants to book an appointment.", "type": "function", "url": "https://api.example.com/webhooks/availability", "parameters": { "type": "object", "properties": { "date": { "type": "string", "description": "Date to check in YYYY-MM-DD format" }, "service": { "type": "string", "description": "Type of service requested" } }, "required": ["date"] } } ``` ### Book Appointment [#book-appointment] ```json { "name": "book_appointment", "description": "Book an appointment for the customer. Only use after confirming the date, time, and service with the customer.", "type": "function", "url": "https://api.example.com/webhooks/book", "parameters": { "type": "object", "properties": { "date": { "type": "string", "description": "Appointment date in YYYY-MM-DD format" }, "time": { "type": "string", "description": "Appointment time in HH:MM format" }, "service": { "type": "string" }, "customer_name": { "type": "string" }, "notes": { "type": "string" } }, "required": ["date", "time", "customer_name"] } } ``` ### Log Event (Async) [#log-event-async] ```json { "name": "log_interest", "description": "Log that the customer expressed interest in a product for sales follow-up", "type": "function", "url": "https://api.example.com/webhooks/log", "async": true, "async_response": "I've noted your interest for our team to follow up.", "parameters": { "type": "object", "properties": { "product": { "type": "string", "description": "Product the customer is interested in" }, "notes": { "type": "string", "description": "Additional notes about the interest" } }, "required": ["product"] } } ``` ### Multi-Destination Transfer [#multi-destination-transfer] ```json { "name": "transfer_call", "description": "Transfer the call to the appropriate department", "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Sales team - for pricing, quotes, and new business", "message": "I'm transferring you to our sales team." }, { "type": "number", "number": "+31687654321", "description": "Technical support - for product issues and troubleshooting", "message": "I'm transferring you to technical support." }, { "type": "number", "number": "+31698765432", "description": "Billing - for invoice and payment questions", "message": "I'm transferring you to the billing department." } ] } ``` ## Listing Templates [#listing-templates] ```bash curl https://api.hmsovereign.com/api/v1/tool-templates \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Updating a Template [#updating-a-template] ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/tool-templates/TEMPLATE_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated description for better LLM understanding" }' ``` ## Deleting a Template [#deleting-a-template] ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/tool-templates/TEMPLATE_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` > **Note**: Deleting a template does not affect assistants currently using it. Tools are stored as objects in the assistant configuration, not as references. *** ## Related [#related] * [Custom Tools Guide](./custom-tools.md) * [Tool Templates API](/docs/api/tool-templates/listToolTemplates) * [Tool Calls Webhook](/docs/webhooks/tool-calls) # AI Generation Source: https://doc.voicedock.ai/docs/features/ai-generation HMS Sovereign includes AI-powered generation tools that help you configure your voice assistants faster. Instead of writing system prompts, analysis schemas, and workflow integrations from scratch, describe what you need in plain language and let the AI generate production-ready configurations for you. **AI Generation** accelerates your setup by turning natural-language descriptions into complete assistant prompts, tool definitions, analysis templates, and importable n8n workflows. *** ## Overview [#overview] The AI generation features are available directly in the HMS Sovereign dashboard when creating or editing an assistant. There are two main generators: * **Prompt Generator** - Generate system prompts, tools, and analysis templates from a description of what your assistant should do * **n8n Workflow Generator** - Generate complete, importable n8n workflow JSON for any tool your assistant uses *** ## Prompt Generator [#prompt-generator] The Prompt Generator creates a complete voice-optimized system prompt for your assistant based on a natural-language description of what the assistant should do. Optionally, it can also generate matching tool definitions and an analysis template in a single step. ### How It Works [#how-it-works] ### Open the Prompt Generator [#open-the-prompt-generator] Navigate to the assistant editor in the HMS Sovereign dashboard. Click the **Generate with AI** button in the system prompt section. ### Describe Your Assistant [#describe-your-assistant] Write a description of what your assistant should do. You can type or use the built-in voice input to dictate your description. Be as detailed or as brief as you like -- the generator will fill in the gaps with best practices for voice AI. **Example input:** ```text I need an assistant for a dental clinic. It should answer questions about our services (whitening, implants, cleanings), check appointment availability, and book appointments. If someone has an urgent issue, transfer them to our front desk. The clinic is open Monday to Friday, 9am to 5pm. The assistant should speak Dutch. ``` ### Enable Generate Extras (Optional) [#enable-generate-extras-optional] Toggle **Also generate tools & analysis** to have the AI suggest tool definitions and an analysis template alongside the prompt. When enabled, the generator will: * Propose custom **function tools** based on the actions described (e.g., `check_availability`, `book_appointment`) * Suggest **end call** and **transfer call** tools if appropriate * Create a matching **analysis template** with a JSON schema tailored to your use case ### Review the Output [#review-the-output] The generator returns: * **System prompt** -- A voice-optimized prompt following best practices for rhythm, pacing, phonetic clarity, and conversational flow * **Explanation note** -- A summary of the design decisions made * **Tools** (if extras enabled) -- Ready-to-use tool definitions with names, descriptions, and parameter schemas * **Analysis template** (if extras enabled) -- A complete analysis configuration with schema and prompts ### Accept or Refine [#accept-or-refine] Review the generated output. You can: * **Accept** everything as-is to apply it to your assistant * **Selectively accept** -- choose which tools and whether to include the analysis template * **Retry with feedback** -- provide specific feedback and regenerate for a better result * **Edit manually** after accepting to make fine-tuned adjustments ### Voice Input [#voice-input] The Prompt Generator supports voice input via the microphone button. Click it to start recording, speak your description naturally, and click again to stop. The transcription is added to the text input in real time, so you can combine voice and typed input. > **Tip: Pro Tip** Voice input works especially well for this feature -- describe your assistant the same way you'd explain it to a colleague. The AI handles the translation into a structured voice assistant prompt. ### What Makes Generated Prompts Special [#what-makes-generated-prompts-special] Generated prompts follow voice AI best practices automatically: * **Short sentences** -- Kept under 15 words for natural phone delivery * **Pacing markers** -- Commas used as breathing pauses for the TTS engine * **Phonetic clarity** -- Acronyms and brand names spelled out for speech * **Human fillers** -- Natural phrases like "Let me check that for you" distributed throughout * **Signposting** -- The assistant narrates what it's doing when using tools or looking things up * **No visual formatting** -- No markdown, URLs, bullets, or special characters that can't be spoken * **Structured flow** -- Identity, context, personality, boundaries, conversation flow, tool instructions, edge cases, and sign-off ### Generated Tools [#generated-tools] When you enable the extras toggle, the generator analyzes your description and creates tool definitions that match your described use case. Each generated tool includes: | Field | Description | | ------------- | -------------------------------------------------------------- | | `name` | A descriptive function name (e.g., `check_availability`) | | `description` | Clear instructions for the LLM on when and how to use the tool | | `type` | `function`, `end_call`, or `transfer_call` | | `parameters` | A JSON Schema defining the expected input parameters | | `reasoning` | An explanation of why this tool was suggested | You can select which tools to keep before accepting. Accepted tools are saved as [tool templates](../configuration/custom-tools.md) in your organization and automatically attached to the assistant. ### Generated Analysis Templates [#generated-analysis-templates] The generator can also create an analysis template that extracts structured data from calls handled by your assistant. The generated template includes: | Field | Description | | --------------- | ----------------------------------------------------------------------------------------------- | | `name` | A descriptive template name | | `description` | What this analysis extracts | | `schema` | A JSON Schema defining the structured output format | | `system_prompt` | Instructions for the AI analyzer | | `user_prompt` | The user-facing prompt with `{{schema}}`, `{{transcript}}`, and `{{ended_reason}}` placeholders | | `reasoning` | An explanation of why this schema was designed this way | The generated analysis template is saved to your organization's [analysis templates](./call-analysis.md) and can be reused across multiple assistants. ### Example [#example] **Input:** ```text A customer support assistant for an e-commerce store. It should look up order status, process return requests, and escalate complex issues to a human. Speak English. Track whether the customer's issue was resolved. ``` **Generated prompt (excerpt):** ```text You are a customer support assistant for [Company Name]. You help callers with order inquiries, returns, and general questions. Your tone is warm and professional. You speak clearly and at a comfortable pace. When a caller asks about an order, use the check order status tool. Read the status back to them and ask if there's anything else you can help with. For return requests, use the process return tool. Confirm the order number and reason before submitting... ``` **Generated tools:** ```json [ { "name": "check_order_status", "type": "function", "description": "Look up the current status of a customer order by order ID. Use when the caller asks about their order, delivery, or shipment.", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order ID provided by the customer" } }, "required": ["order_id"] } }, { "name": "process_return", "type": "function", "description": "Submit a return request for a customer order. Confirm the order ID and return reason with the customer before calling this tool.", "parameters": { "type": "object", "properties": { "order_id": { "type": "string" }, "reason": { "type": "string", "description": "Reason for the return" } }, "required": ["order_id", "reason"] } }, { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+1234567890", "description": "Human support for complex issues that cannot be resolved by the AI" } ] }, { "type": "end_call" } ] ``` **Generated analysis template:** ```json { "name": "E-Commerce Support Analysis", "schema": { "type": "object", "properties": { "issue_resolved": { "type": "boolean" }, "issue_category": { "type": "string", "enum": ["order_status", "return", "complaint", "general_inquiry", "other"] }, "customer_sentiment": { "type": "integer", "minimum": 1, "maximum": 10 }, "escalated": { "type": "boolean" }, "summary": { "type": "string" } } } } ``` *** ## n8n Workflow Generator [#n8n-workflow-generator] The n8n Workflow Generator creates complete, importable n8n workflow JSON for any tool configured on your assistant. Instead of building webhook-based workflows from scratch, the generator produces a ready-to-import workflow with all the nodes, connections, and documentation you need. ### How It Works [#how-it-works-1] ### Open the Workflow Generator [#open-the-workflow-generator] In the assistant editor, navigate to the **Tools** section. Click the **n8n** button on any function tool to open the workflow generator. ### Review the Tool Configuration [#review-the-tool-configuration] The generator shows a summary of the selected tool, including its name, description, parameter count, and URL configuration. It also has access to the assistant's system prompt for additional context. ### Add Custom Instructions (Optional) [#add-custom-instructions-optional] Provide additional context for the workflow, such as: * Which external APIs or services the workflow should connect to * Specific data transformations or business logic * Error handling preferences * Any credentials or authentication requirements ```text Connect to our Airtable CRM base to look up customer records. The base ID is appXXXXXX and the table is called "Customers". Match on the phone number field. ``` ### Generate the Workflow [#generate-the-workflow] Click **Generate Workflow**. The AI analyzes the tool configuration and creates a complete n8n workflow. This typically takes 30-60 seconds. ### Review and Export [#review-and-export] The generator returns: * **Workflow notes** -- Documentation explaining what the workflow does, what credentials are needed, and setup steps * **Workflow JSON** -- The complete n8n workflow ready to import You can **copy** the JSON to your clipboard or **download** it as a `.json` file. ### Generated Workflow Structure [#generated-workflow-structure] Every generated workflow follows the HMS Sovereign webhook contract and includes: | Component | Description | | --------------------------- | ---------------------------------------------------------------------------------------- | | **Webhook node** | A POST webhook that receives tool call payloads from HMS Sovereign | | **Processing nodes** | Logic nodes (HTTP Request, Code, IF, Switch, etc.) tailored to what the tool needs to do | | **Respond to Webhook node** | Returns the result in the format HMS Sovereign expects | | **Sticky Note** | Documentation describing the workflow, required credentials, and setup instructions | | **Pin data** | Realistic sample data on the Webhook node for testing | ### Importing into n8n [#importing-into-n8n] ### Copy or Download the JSON [#copy-or-download-the-json] Use the **Copy JSON** or **Download JSON** button in the generator. ### Open n8n [#open-n8n] Navigate to your n8n instance and open the workflow editor. ### Import the Workflow [#import-the-workflow] In n8n, press `Ctrl+V` (or `Cmd+V` on Mac) to paste the workflow, or use **Import from File** if you downloaded the JSON. ### Configure Credentials [#configure-credentials] The workflow's Sticky Note documents which credentials are needed. Add your credentials in n8n's **Credentials** section and connect them to the relevant nodes. ### Update the Webhook URL [#update-the-webhook-url] After importing, n8n assigns a unique webhook URL to the Webhook node. Copy this URL and set it as the tool's `url` in your HMS Sovereign assistant configuration. ### Activate and Test [#activate-and-test] Activate the workflow in n8n. Make a test call to your assistant and trigger the tool to verify everything works end-to-end. ### Webhook Payload Format [#webhook-payload-format] All tool call webhooks from HMS Sovereign follow this format: ```json { "message": { "type": "tool-calls", "call": { "id": "call-uuid", "type": "inbound_phone_call", "status": "in-progress" }, "phone_number": { "number": "+31850046559", "name": "Company Name" }, "customer": { "number": "+31623731537" }, "tool_call_list": [ { "id": "tool_a1b2c3d4e5f6", "type": "function", "function": { "name": "tool_name", "arguments": { "param1": "value1" } } } ] } } ``` Key paths for use in n8n expressions: | Path | Description | | --------------------------------------------------- | --------------------------------------------------- | | `body.message.tool_call_list[0].function.arguments` | The tool's input parameters | | `body.message.tool_call_list[0].id` | The tool call ID (must be returned in the response) | | `body.message.customer.number` | The caller's phone number | | `body.message.call.id` | The unique call ID | ### Required Response Format [#required-response-format] Your n8n workflow must return this JSON structure via the **Respond to Webhook** node: ```json { "results": [ { "tool_call_id": "tool_a1b2c3d4e5f6", "result": "Human-readable result for the voice assistant to speak" } ] } ``` > **WarningImportant**: The `tool_call_id` must match the incoming `tool_call_list[0].id`. The `result` string is what the assistant reads aloud to the caller. For async tools (where `async` is set to `true` in the tool configuration), the response content is ignored -- just return HTTP 200. ### Example Use Cases [#example-use-cases] #### CRM Lookup via Airtable [#crm-lookup-via-airtable] Generate a workflow for a `lookup_customer` tool that queries an Airtable base by phone number and returns customer details for the assistant to reference during the call. #### Appointment Booking via Google Calendar [#appointment-booking-via-google-calendar] Generate a workflow for a `book_appointment` tool that checks Google Calendar availability, creates an event, and confirms the booking back to the caller. #### Order Status via REST API [#order-status-via-rest-api] Generate a workflow for a `check_order_status` tool that calls your e-commerce API with an order ID and returns the current shipping status. #### Support Ticket Creation via Zendesk [#support-ticket-creation-via-zendesk] Generate a workflow for a `create_ticket` tool that creates a Zendesk ticket with the call summary and customer details after the caller describes their issue. #### Lead Logging via Webhook [#lead-logging-via-webhook] Generate a workflow for an async `log_lead` tool that sends lead data to your CRM or marketing platform without blocking the conversation. *** ## Best Practices [#best-practices] * **Prompt Generation** - - Be specific about the assistant's purpose, language, and personality * Mention all tools and actions the assistant should be able to perform * Include business rules and constraints (hours, policies, limits) * Use the retry-with-feedback feature to iteratively refine the output * Always review and edit the generated prompt before going live * **n8n Workflow Generation** - - Provide clear custom instructions about which external services to connect * Always test the imported workflow with the included pin data before activating * Review the Sticky Note for required credentials and setup steps * Update placeholder URLs and credentials with your actual values * Test end-to-end by making a real call after activating the workflow > \*\*Tip: \*\* The AI generators are a starting point, not a finished product. Always review and customize the output to match your exact requirements before deploying to production. *** ## Costs [#costs] Both generators use AI processing to create their output. Each generation counts toward your organization's AI usage. The cost is displayed in the generator dialog before you confirm. > **Note**: Generation costs are separate from per-call costs. You are only charged when you click **Generate**, not when reviewing or editing the output. *** ## Related [#related] * [Custom Tools Guide](../configuration/custom-tools.md) * [Tool Templates Guide](../configuration/custom-tools.md) * [Call Analysis Guide](./call-analysis.md) * [Analysis Templates Guide](./call-analysis.md) * [Tool Calls Webhook](../webhooks/tool-calls.md) * [Assistants API](../core-concepts/assistants.md) # Autonomous Silence Handling Source: https://doc.voicedock.ai/docs/features/autonomous-silence-handling > Technical documentation for the `autonomous_silence_handling` feature. ## Overview [#overview] Autonomous Silence Handling is a feature that allows the AI assistant to intelligently detect and respond to prolonged silence during a call. When enabled, the assistant will: 1. Detect when the caller has stopped speaking 2. Prompt the caller to check if they're still on the line 3. After multiple attempts without response, gracefully end the call This prevents calls from hanging indefinitely when callers walk away, get disconnected, or put the phone down. ## Configuration [#configuration] ### Assistant Schema [#assistant-schema] ```json { "autonomous_silence_handling": true } ``` | Field | Type | Default | Description | | ----------------------------- | ------- | ------- | ------------------------------------------- | | `autonomous_silence_handling` | boolean | `false` | Enable/disable autonomous silence detection | ### Automatic Tool Addition [#automatic-tool-addition] When `autonomous_silence_handling` is enabled, the system automatically adds the `end_call` tool to the assistant's toolset if not already present. This ensures the assistant can hang up after detecting prolonged silence. ## How It Works [#how-it-works] ### Timing Configuration [#timing-configuration] | Parameter | Value | Description | | ------------------------ | ---------- | ------------------------------------------------------- | | `first_silence_check` | 7 seconds | Time before first prompt after silence detected | | `silence_check_interval` | 12 seconds | Time between subsequent prompts | | `max_silence_attempts` | 3 | Maximum prompts before instructing assistant to hang up | ### State Machine [#state-machine] ``` [User Speaking] → [User Away] → [Silence Check 1] → [Silence Check 2] → [Silence Check 3] → [Hang Up] ↑ │ │ │ │ └──────────────┴────────────────┴───────────────────┴───────────────────┘ (User speaks → reset timer) ``` ### Event Flow [#event-flow] 1. **User State: "away"** - Silence timer starts 2. **7 seconds pass** - First silence check triggered 3. **Assistant speaks** - "Are you still there?" (contextual) 4. **12 more seconds** - Second silence check 5. **Assistant speaks** - "I'm still here if you need me" 6. **12 more seconds** - Third and final check 7. **Assistant speaks** - Brief goodbye + calls `end_call` tool ### Contextual Prompts [#contextual-prompts] The assistant receives different instructions based on the attempt number: **Attempt 1:** ``` IMPORTANT: No speech detected from caller. This is attempt 1 of 3. Based on context: if they said they need to look something up, say you'll wait. Otherwise, ask a short question to check if they are still on the line. ``` **Attempt 2:** ``` IMPORTANT: Still no speech from caller. This is attempt 2 of 3. Ask one more time if they are still there. If no response after this, you will need to end the call. ``` **Attempt 3 (Final):** ``` IMPORTANT: No speech detected from caller for a long time. This is attempt 3 of 3. You must now say a brief goodbye and immediately use end_call to hang up. ``` ## Key Behaviors [#key-behaviors] 1. **No check while assistant speaking**: Silence timer pauses when assistant is speaking 2. **Reset on user speech**: Any detected user speech resets the timer and attempt counter 3. **Graceful termination**: Assistant is instructed to say goodbye before hanging up 4. **Context-aware**: First prompt considers conversation context (e.g., "let me look that up") ## Use Cases [#use-cases] ### Good For [#good-for] * **Reception/Front Desk**: Callers may walk away to check something * **Customer Support**: Long hold times while customers search for info * **Appointment Booking**: Callers checking their calendars ### Consider Disabling For [#consider-disabling-for] * **Emergency Lines**: May need to stay connected even during silence * **Listening Services**: Where silence is expected and acceptable * **IVR/Menu Systems**: Where users navigate with keypresses, not speech ## Interaction with Other Features [#interaction-with-other-features] ### With `voicemail_detection` [#with-voicemail_detection] Both can be enabled simultaneously. Voicemail detection triggers first (on initial greeting), while silence handling activates during the conversation. ### With `max_duration_seconds` [#with-max_duration_seconds] If `max_duration_seconds` is set, the call will end at that time regardless of silence handling state. ### With `end_call` Tool [#with-end_call-tool] If the assistant already has `end_call` in its tools, no duplicate is added. The silence handling instructions simply tell the assistant to use the existing tool. ## Webhook Events [#webhook-events] When a call ends due to silence handling: ```json { "message": { "type": "end-of-call-report", "call": { "status": "ended" }, "summary": "Call ended after no response from caller..." } } ``` The `status-update` webhook will show `status: "ended"` (not a special silence-related status). ## Troubleshooting [#troubleshooting] ### Assistant Not Detecting Silence [#assistant-not-detecting-silence] * Verify `autonomous_silence_handling: true` is in the assistant configuration * Check the call transcript to see if silence detection prompts appear * Ensure STT (Speech-to-Text) is properly configured to detect when user stops speaking ### Assistant Ending Calls Too Quickly [#assistant-ending-calls-too-quickly] * The 7-second initial delay should prevent premature endings * Review the call transcript to see timing of silence prompts * Note that timing parameters (7s, 12s, 3 attempts) are currently fixed ### Assistant Not Hanging Up After 3 Attempts [#assistant-not-hanging-up-after-3-attempts] * The `end_call` tool is automatically added when silence handling is enabled * Review the end-of-call-report to see if the assistant attempted to end the call * The LLM may occasionally not follow instructions perfectly - this is expected behavior with AI assistants # Call Analysis Source: https://doc.voicedock.ai/docs/features/call-analysis Call analysis extracts structured data from call transcripts using AI. Use it to automatically capture customer sentiment, intents, outcomes, and any other data you need. ## How It Works [#how-it-works] 1. Configure an `analysis_plan` on your assistant 2. After each call, the transcript is analyzed 3. Structured data matching your schema is extracted 4. Results are included in the end-of-call report webhook ## Basic Configuration [#basic-configuration] Add an analysis plan to your assistant: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "analysis_plan": { "structured_data_plan": { "enabled": true, "schema": { "type": "object", "properties": { "sentiment": { "type": "integer", "minimum": 1, "maximum": 10, "description": "Customer sentiment score" }, "intent": { "type": "string", "enum": ["support", "sales", "complaint", "inquiry", "other"] }, "resolved": { "type": "boolean", "description": "Was the customer issue resolved" } } }, "messages": [ { "role": "system", "content": "Analyze the call transcript and extract structured data according to the schema." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript: {{transcript}}" } ] }, "min_messages_threshold": 3 } }' ``` ## Schema Design [#schema-design] Define the exact data you want to extract using JSON Schema: ```json { "type": "object", "properties": { "sentiment": { "type": "integer", "minimum": 1, "maximum": 10, "description": "Overall customer sentiment (1=very negative, 10=very positive)" }, "primary_intent": { "type": "string", "enum": ["support", "sales", "complaint", "inquiry", "appointment", "other"], "description": "Main reason for the call" }, "topics_discussed": { "type": "array", "items": { "type": "string" }, "description": "List of main topics covered in the conversation" }, "action_items": { "type": "array", "items": { "type": "object", "properties": { "task": { "type": "string" }, "assigned_to": { "type": "string", "enum": ["assistant", "customer", "other"] } } } }, "customer_satisfied": { "type": "boolean" }, "follow_up_needed": { "type": "boolean" }, "summary": { "type": "string", "description": "Brief summary of the call outcome" } } } ``` ## Prompt Templates [#prompt-templates] The `messages` array defines how the transcript is analyzed. Use placeholders: | Placeholder | Description | | ------------------ | -------------------------------- | | `{{schema}}` | Your JSON Schema (auto-injected) | | `{{transcript}}` | Full call transcript | | `{{ended_reason}}` | Why the call ended | ### Example Prompts [#example-prompts] **Basic analysis:** ```json { "messages": [ { "role": "system", "content": "You are a call analyst. Extract structured data from the call transcript according to the provided schema. Return valid JSON only." }, { "role": "user", "content": "Schema: {{schema}}\n\nCall transcript:\n{{transcript}}\n\nThe call ended because: {{ended_reason}}" } ] } ``` **Detailed analysis with instructions:** ```json { "messages": [ { "role": "system", "content": "Analyze customer service calls and extract data according to the schema. Guidelines:\n- Sentiment: 1-3 negative, 4-6 neutral, 7-10 positive\n- Only mark resolved=true if the issue was fully addressed\n- Include all mentioned topics, not just the main one" }, { "role": "user", "content": "Schema:\n{{schema}}\n\nTranscript:\n{{transcript}}\n\nCall ended: {{ended_reason}}\n\nProvide your analysis as JSON." } ] } ``` ## Minimum Messages Threshold [#minimum-messages-threshold] Set `min_messages_threshold` to skip analysis for very short calls: ```json { "min_messages_threshold": 5 } ``` * Default: 2 messages * Set higher for meaningful analysis * Analysis is skipped if threshold not met ## Receiving Analysis Results [#receiving-analysis-results] Analysis results are included in the end-of-call report webhook: ```json { "message": { "type": "end-of-call-report", "duration_seconds": 84, "summary": "Customer asked about opening hours...", "analysis": { "sentiment": 8, "primary_intent": "inquiry", "topics_discussed": ["opening hours", "location", "parking"], "customer_satisfied": true, "follow_up_needed": false, "summary": "Customer received all requested information." } } } ``` ## Use Cases [#use-cases] ### Customer Satisfaction Tracking [#customer-satisfaction-tracking] ```json { "schema": { "type": "object", "properties": { "csat_score": { "type": "integer", "minimum": 1, "maximum": 5 }, "would_recommend": { "type": "boolean" }, "pain_points": { "type": "array", "items": { "type": "string" } } } } } ``` ### Sales Lead Qualification [#sales-lead-qualification] ```json { "schema": { "type": "object", "properties": { "lead_quality": { "type": "string", "enum": ["hot", "warm", "cold"] }, "budget_mentioned": { "type": "boolean" }, "timeline": { "type": "string" }, "decision_maker": { "type": "boolean" }, "products_interested": { "type": "array", "items": { "type": "string" } } } } } ``` ### Support Ticket Classification [#support-ticket-classification] ```json { "schema": { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "technical", "account", "product", "other"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "resolution_status": { "type": "string", "enum": ["resolved", "escalated", "pending", "unresolved"] }, "root_cause": { "type": "string" } } } } ``` ## Best Practices [#best-practices] 1. **Keep schemas focused** - Extract only what you'll actually use 2. **Use enums** - Predefined values make data more consistent 3. **Add descriptions** - Help the AI understand what you want 4. **Test prompts** - Refine your prompts based on actual results 5. **Set appropriate thresholds** - Skip analysis for calls too short to be meaningful See [Analysis Templates](/docs/configuration/analysis-templates) and [End of Call Report Webhook](/docs/webhooks/end-of-call-report) for complete details. # Call Transfers Source: https://doc.voicedock.ai/docs/features/call-transfers Transfer calls from your AI assistant to human agents, other departments, or external numbers using SIP trunks or the built-in transfer tool. ## Transfer Methods [#transfer-methods] ### 1. Assistant-Initiated Transfer (Transfer Tool) [#1-assistant-initiated-transfer-transfer-tool] Let your AI assistant decide when to transfer based on the conversation. ### 2. API-Initiated Transfer (Call Control) [#2-api-initiated-transfer-call-control] Transfer calls programmatically via the API. ## Setting Up Assistant Transfers [#setting-up-assistant-transfers] ### Step 1: Add Transfer Tool [#step-1-add-transfer-tool] Add the transfer tool to your assistant's configuration: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "llm_config": { "tools": [ { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Sales team - for pricing and quotes", "message": "I'm transferring you to our sales team, one moment please." }, { "type": "number", "number": "+31687654321", "description": "Technical support - for product issues and troubleshooting", "message": "I'm transferring you to technical support." }, { "type": "number", "number": "+31698765432", "description": "Billing department - for invoice and payment questions", "message": "I'm transferring you to the billing department." } ] } ] } }' ``` ### Step 2: Guide the Assistant [#step-2-guide-the-assistant] Update your system prompt to guide when to transfer: ```json { "messages": [ { "role": "system", "content": "You are a customer service agent. You can answer most questions yourself, but transfer to:\n- Sales: for questions about pricing, quotes, or new products\n- Technical support: for technical issues you cannot resolve\n- Billing: for specific billing questions\n\nAlways try to help first before transferring." } ] } ``` ### Transfer Tool Schema [#transfer-tool-schema] | Field | Type | Required | Description | | --------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------- | | `type` | string | Yes | `transfer_call` | | `destinations` | array | Yes | List of transfer destinations | | `destinations[].type` | string | Yes | `number` | | `destinations[].number` | string | Yes | Phone number in E.164 format | | `destinations[].description` | string | Yes | When to use this destination (for LLM) | | `destinations[].message` | string | No | Message spoken before transfer | | `wait_for_answer` | boolean | No | Wait for the destination to answer before connecting the caller (default `false`, see below) | | `answer_timeout_seconds` | number | No | Max ring time before giving up (default `20`, only with `wait_for_answer`) | | `fallback_destinations` | array | No | Backup numbers tried in order on no answer (only with `wait_for_answer`) | | `fallback_destinations[].number` | string | Yes | Phone number in E.164 format | | `fallback_destinations[].message` | string | No | Announcement before dialling this backup; silent dial if omitted | ## No-Answer Handling (Monitored Transfers) [#no-answer-handling-monitored-transfers] By default a transfer is *blind*: the caller is connected to the destination as soon as it starts ringing. If nobody picks up, the caller ends up in the destination's voicemail — and the assistant is no longer there to help. Set `wait_for_answer: true` to make the transfer *monitored*. The destination is dialled first, and the caller is only connected once someone actually answers: * **Answered** → the call is bridged exactly as with a blind transfer. * **No answer within `answer_timeout_seconds`, busy, or declined** → the assistant stays with the caller and receives a tool result telling it what happened (`no answer` vs `busy`), so it can take a message, offer an alternative, or end the call politely. It is explicitly instructed never to claim the transfer succeeded. Optionally add `fallback_destinations`: when the chosen destination does not answer, each backup number is tried in order, and the first one to pick up gets the call. Only when every number fails does control return to the assistant. ```json { "type": "transfer_call", "wait_for_answer": true, "answer_timeout_seconds": 18, "destinations": [ { "type": "number", "number": "+31612345678", "description": "Building manager - for urgent repairs", "message": "I'm connecting you to the building manager, one moment please." } ], "fallback_destinations": [ { "number": "+31687654321", "message": "Trying the emergency line instead." } ] } ``` > **Voicemail counts as an answer.** When a destination's voicemail picks up, the phone network reports the call as answered — indistinguishable from a human. Set `answer_timeout_seconds` *below* the destination's ring-to-voicemail delay (typically 20–30 seconds) so the assistant pulls back before voicemail answers. Existing transfer tools are unaffected: without `wait_for_answer` the behaviour is the blind transfer described above, unchanged. ## API-Initiated Transfers [#api-initiated-transfers] Transfer an active call via the Call Control API: ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "transfer", "destination": "+31612345678", "message": "One moment, I'm transferring you to a colleague." }' ``` ### Transfer Command Fields [#transfer-command-fields] | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------- | | `type` | string | Yes | `transfer` | | `destination` | string | Yes | Phone number or SIP URI | | `message` | string | No | Message before transfer | ## Using SIP Trunks [#using-sip-trunks] For transfers via your own phone system, configure a SIP trunk. ### Step 1: Create SIP Trunk [#step-1-create-sip-trunk] ```bash curl -X POST https://api.hmsovereign.com/api/v1/sip-trunks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Office PBX", "host": "pbx.example.com", "port": 5060, "username": "hms-sovereign", "password": "secure-password" }' ``` ### Step 2: Assign to Phone Number [#step-2-assign-to-phone-number] ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "transfer_trunk_id": "TRUNK_ID" }' ``` Now transfers from this number will route through your SIP trunk. ## Transfer Best Practices [#transfer-best-practices] 1. **Clear descriptions** - Help the assistant know exactly when to transfer 2. **Informative messages** - Tell callers where they're being transferred 3. **Avoid over-transferring** - Train your assistant to handle common questions 4. **Test destinations** - Verify all transfer numbers are working 5. **Monitor transfers** - Track transfer rates to identify training opportunities ## Example: Complete Transfer Setup [#example-complete-transfer-setup] ```json { "name": "Support Assistant", "first_message": "Welcome to Acme support, how can I help you?", "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "messages": [ { "role": "system", "content": "You are a support agent for Acme. Help customers with questions about products and services. Transfer only if:\n1. The customer explicitly asks for a human\n2. You cannot answer a question after 2 attempts\n3. There is a complaint that requires escalation" } ], "tools": [ { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Human support assistant for complex issues or when customer explicitly requests a human", "message": "I'm transferring you to one of my colleagues, one moment please." } ] }, { "type": "end_call" } ] } } ``` See [Call Control API](/docs/api/call-control/controlCall) and [SIP Trunks API](/docs/api/sip-trunks/listSipTrunks) for complete details. # Campaigns Setup Source: https://doc.voicedock.ai/docs/features/campaigns-setup ## Overview [#overview] Outbound campaigns allow you to upload a list of leads and have the system automatically call them sequentially with personalized context. ## Architecture [#architecture] **Flow:** 1. User creates campaign with CSV of leads 2. User starts campaign (status → `scheduled`) 3. Edge Function processes first lead 4. Call happens via LiveKit droplet 5. When call ends, database trigger fires 6. Trigger calls Edge Function to process next lead 7. Repeat until all leads processed **Components:** * **Next.js UI**: Campaign creation, management, monitoring * **Supabase Database**: Campaigns, leads, triggers * **Supabase Edge Function**: Campaign processing logic * **LiveKit Droplet**: Call execution ## Setup Instructions [#setup-instructions] ### 1. Deploy Edge Function [#1-deploy-edge-function] ```bash cd supabase supabase functions deploy process-campaign ``` ### 2. Configure Environment Variables [#2-configure-environment-variables] In Supabase Dashboard → Settings → Edge Functions: * `DROPLET_INTERNAL_URL`: Your LiveKit droplet URL (e.g., `http://142.93.xxx.xxx:8082`) * `DROPLET_INTERNAL_KEY`: Your droplet internal authentication key ### 3. Configure Database [#3-configure-database] Run these SQL commands in Supabase SQL Editor: ```sql -- Set Edge Function URL (replace YOUR_PROJECT_REF) ALTER DATABASE postgres SET app.supabase_functions_url = 'https://YOUR_PROJECT_REF.supabase.co/functions/v1'; -- Set service role key (from Dashboard → Settings → API) ALTER DATABASE postgres SET app.supabase_service_role_key = 'eyJh...'; -- Verify SELECT name, setting FROM pg_settings WHERE name LIKE 'app.%'; ``` ### 4. Optional: Cron Backup [#4-optional-cron-backup] For extra reliability, add a cron job: **Dashboard → Integrations → Cron:** * Name: `process-campaigns-backup` * Schedule: `*/2 * * * *` * Type: HTTP Request * Method: POST * URL: `https://YOUR_PROJECT_REF.supabase.co/functions/v1/process-campaign` * Headers: ```json { "Authorization": "Bearer YOUR_SERVICE_ROLE_KEY", "Content-Type": "application/json" } ``` * Body: Check all active campaigns (requires Edge Function modification) ## Campaign Lifecycle [#campaign-lifecycle] **Status Flow:** * `draft` → Campaign being created * `scheduled` → Active, will process leads during time window * `paused` → Manually paused by user * `completed` → All leads processed **Lead Status Flow:** * `pending` → Waiting to be called * `calling` → Call in progress * `completed` → Call finished successfully * `failed` → Call failed or errored ## Monitoring [#monitoring] **View Edge Function Logs:** ```bash supabase functions logs process-campaign --tail ``` **Check Campaign Stats:** Query the `campaigns` table for `completed_leads`, `failed_leads`, and `total_leads`. **Debug Stuck Campaigns:** Check `last_lead_processed_at` - if older than 5 minutes and status is `scheduled`, campaign may be stuck. ## Troubleshooting [#troubleshooting] **Campaigns not processing:** 1. Check Edge Function deployment: `supabase functions list` 2. Verify database settings: `SELECT name, setting FROM pg_settings WHERE name LIKE 'app.%';` 3. Check Edge Function logs for errors 4. Verify droplet is accessible from Supabase **Calls not initiating:** 1. Check droplet logs 2. Verify organization has active phone number and SIP trunk 3. Verify organization has sufficient balance 4. Check Edge Function logs for detailed error **Time window not working:** 1. Verify campaign timezone is correct 2. Check `schedule_start_time` and `schedule_end_time` format (HH:MM:SS) 3. Edge Function logs will show "Outside time window" message # Outbound Campaigns Source: https://doc.voicedock.ai/docs/features/campaigns Outbound Campaigns is currently in **Beta**. Features may change. This guide walks you through setting up an automated outbound calling campaign to reach your leads with personalized AI conversations. ## Overview [#overview] Outbound campaigns allow you to: * Upload a list of leads with contact information * Personalize each call with lead-specific context * Schedule calls within specific time windows * Track progress and results for each lead ```mermaid flowchart LR A[Create Campaign] --> B[Add Leads] B --> C[Set Schedule] C --> D[Start Campaign] D --> E{Time Window?} E -->|Yes| F[Call Next Lead] E -->|No| G[Wait] G --> E F --> H{More Leads?} H -->|Yes| E H -->|No| I[Campaign Complete] ``` *** ## Prerequisites [#prerequisites] Before creating a campaign, ensure you have: 1. **An active assistant** - See [Assistants API](../core-concepts/assistants.md) 2. **A phone number** - See [Phone Numbers API](../core-concepts/phone-numbers.md) 3. **A configured SIP trunk** - The phone number must have a SIP trunk configured for outbound calls > **Warning**: The phone number used for outbound calls must have a SIP trunk configured for outbound dialing. *** ## Quick Start [#quick-start] ### Create a Campaign [#create-a-campaign] Create a campaign linked to your assistant: ```bash curl -X POST https://api.hmsovereign.com/api/v1/campaigns \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 Sales Outreach", "assistant_id": "YOUR_ASSISTANT_ID", "system_message_template": "You are calling {{name}} from {{company}}. They are interested in {{interest}}.", "schedule_start_time": "09:00:00", "schedule_end_time": "17:00:00", "timezone": "Europe/Amsterdam" }' ``` ### Add Leads [#add-leads] Add leads with personalization variables: ```bash curl -X POST https://api.hmsovereign.com/api/v1/campaigns/CAMPAIGN_ID/leads \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+31612345678", "variables": { "name": "Jan de Vries", "company": "TechCorp", "interest": "enterprise AI solutions" } }' ``` ### Start the Campaign [#start-the-campaign] Set the campaign status to `scheduled` to begin calling: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/campaigns/CAMPAIGN_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "scheduled" }' ``` ### Monitor Progress [#monitor-progress] Check campaign progress and lead statuses: ```bash curl https://api.hmsovereign.com/api/v1/campaigns/CAMPAIGN_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` *** ## Personalizing Calls [#personalizing-calls] ### System Message Template [#system-message-template] The `system_message_template` is appended to your assistant's existing system messages. Use double curly brace placeholders (e.g. `{{variable}}`) that match keys in each lead's `variables` object. **Example Template:** ``` You are calling {{name}}, the {{role}} at {{company}}. Background: - They expressed interest in: {{interest}} - Previous interaction: {{last_contact}} - Priority: {{priority}} Your goal is to schedule a demo call. ``` **Example Lead Variables:** ```json { "name": "Maria Jansen", "role": "Head of Operations", "company": "LogiFlow BV", "interest": "workflow automation", "last_contact": "Downloaded whitepaper on Jan 5", "priority": "high" } ``` ### Best Practices [#best-practices] > **Tip: Personalization Tips** 1. **Include the lead's name** - Creates immediate rapport 2. **Reference their company** - Shows you've done your research 3. **Mention their interest** - Makes the call relevant 4. **Keep it concise** - Don't overload the assistant with context *** ## Scheduling [#scheduling] ### Time Windows [#time-windows] Campaigns only make calls during the configured time window: | Field | Format | Example | Description | | --------------------- | -------- | ------------------ | ---------------------------- | | `schedule_start_time` | HH:MM:SS | `09:00:00` | Earliest time to start calls | | `schedule_end_time` | HH:MM:SS | `17:00:00` | Latest time to start calls | | `timezone` | IANA | `Europe/Amsterdam` | Timezone for the schedule | ### Supported Timezones [#supported-timezones] Use any valid IANA timezone: * `Europe/Amsterdam` * `Europe/London` * `America/New_York` * `America/Los_Angeles` * `Asia/Tokyo` * etc. > **Note**: Calls are processed one at a time. A new call starts only after the previous call ends. *** ## Campaign Statuses [#campaign-statuses] | Status | Description | Calls Made? | | ----------- | ------------------------------- | ----------- | | `draft` | Campaign created but not active | No | | `scheduled` | Campaign active, calling leads | Yes | | `paused` | Temporarily stopped | No | | `completed` | All leads processed | No | ### Status Transitions [#status-transitions] ```mermaid stateDiagram-v2 [*] --> draft draft --> scheduled: Start campaign scheduled --> paused: Pause paused --> scheduled: Resume scheduled --> completed: All leads done completed --> [*] ``` *** ## Lead Statuses [#lead-statuses] | Status | Description | | ----------- | ------------------------------------ | | `pending` | Waiting to be called | | `calling` | Call currently in progress | | `completed` | Call finished successfully | | `failed` | Call failed (connection error, etc.) | | `no_answer` | No answer after attempts | *** ## Bulk Import [#bulk-import] For large lead lists, you can add multiple leads programmatically: ```javascript const leads = [ { phone_number: "+31612345678", variables: { name: "Jan", company: "A" } }, { phone_number: "+31687654321", variables: { name: "Maria", company: "B" } }, { phone_number: "+31698765432", variables: { name: "Pieter", company: "C" } }, ]; for (const lead of leads) { await fetch(`https://api.hmsovereign.com/api/v1/campaigns/${campaignId}/leads`, { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify(lead) }); } ``` > \*\*Tip: \*\* In the dashboard, you can upload a CSV file to bulk import leads with automatic variable mapping. *** ## Monitoring & Results [#monitoring--results] ### Campaign Progress [#campaign-progress] The campaign object includes progress metrics: ```json { "total_leads": 150, "completed_leads": 45, "current_call_id": "call-uuid-123", "last_lead_processed_at": "2026-01-07T14:25:00.000Z" } ``` ### Call Results [#call-results] After a lead is called, you can retrieve the call details: ```bash curl https://api.hmsovereign.com/api/v1/calls/CALL_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` This includes: * Full transcript * Call duration * Call analysis (if configured on the assistant) * Recording URL (if enabled) See [Calls API](../core-concepts/calls.md) for details. *** ## Error Handling [#error-handling] ### Common Issues [#common-issues] #### Campaign not making calls [#campaign-not-making-calls] Check these conditions: 1. Campaign status is `scheduled` 2. Current time is within the schedule window 3. There are leads with `pending` status 4. Phone number has a valid SIP trunk #### Leads failing immediately [#leads-failing-immediately] Verify: 1. Phone numbers are in E.164 format (`+31612345678`) 2. SIP trunk is configured for outbound calls 3. Assistant is active and properly configured #### Assistant not using lead context [#assistant-not-using-lead-context] Ensure: 1. Variable names in template match keys in lead's `variables` 2. Template uses double curly brace syntax for variables 3. Assistant's LLM is receiving the additional system message *** ## API Reference [#api-reference] * **List Campaigns** - GET /campaigns * **Create Campaign** - POST /campaigns * **Update Campaign** - `PATCH /campaigns/:id` * **Add Lead** - `POST /campaigns/:id/leads` *** ## Related [#related] * [Assistants API](../core-concepts/assistants.md) - Create and configure assistants * [Call Analysis Guide](./call-analysis.md) - Analyze campaign call results # Privacy & Compliance Source: https://doc.voicedock.ai/docs/features/privacy-compliance VoiceDock provides two complementary features for privacy-conscious deployments: **Recording Consent** (DTMF opt-in) and **GDPR Mode**. These can be used independently or together. *** ## Recording Consent (DTMF Opt-In) [#recording-consent-dtmf-opt-in] Recording consent is a pre-call flow that requires callers to actively agree before any recording or AI processing begins. It is designed for use cases where explicit consent is required by law or policy. ### How It Works [#how-it-works] When enabled on an assistant, the following happens at the start of every inbound call: 1. The caller hears a spoken consent message (synthesized by your assistant's TTS provider) 2. **Press 1** — caller agrees. Recording starts and the AI assistant begins the conversation. 3. **Press 2** — caller declines. The call ends immediately. 4. **No response within 10 seconds** — the message is repeated once. If still no response, the call ends. ### What Happens Before Consent [#what-happens-before-consent] No audio is sent to any STT provider, no LLM is invoked, and no recording starts until the caller presses 1. The only external action before consent is the TTS synthesis of the consent message itself. | Action | Before pressing 1 | After pressing 1 | | ---------------------------- | ----------------- | ---------------- | | Audio sent to STT provider | ❌ | ✅ | | LLM invoked | ❌ | ✅ | | Recording started | ❌ | ✅ | | Call stored in dashboard | ✅ (status only) | ✅ (full data) | | TTS used for consent message | ✅ | ✅ | ### Configuration [#configuration] Add `recording_consent` to your assistant configuration: ```json { "recording_consent": { "enabled": true, "message": "This call may be recorded and processed by an AI assistant. Press 1 to agree, or press 2 to hang up." } } ``` Via API: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/{id} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recording_consent": { "enabled": true, "message": "This call may be recorded and processed by an AI assistant. Press 1 to agree, or press 2 to hang up." } }' ``` ### Fields [#fields] | Field | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------------------- | | `enabled` | boolean | Yes | Activates the consent flow | | `message` | string | Yes | The text spoken to the caller. Use the language of your callers. | ### Notes [#notes] * The consent message is spoken by your assistant's configured TTS provider — the same voice used during the conversation * If the caller hangs up before pressing anything, the call is logged as ended. Basic metadata (timestamp, duration, phone numbers) is stored for billing purposes, but no conversation content is recorded or stored. * This feature applies to inbound phone calls only. Web calls skip the consent flow. * DTMF detection uses the standard telephone keypad signals. No voice recognition is involved. *** ## GDPR Mode [#gdpr-mode] GDPR mode is a per-assistant setting that stops VoiceDock from storing conversation content. When enabled, calls are processed normally (the AI speaks, the caller responds), but no transcript, recording, summary, or message history is written to VoiceDock's own systems. Only billing metadata is kept. This governs what **VoiceDock** (operated by Flireo B.V.) retains. It does not by itself control what the AI sub-processors that handle the call in real time do with the data. See [What GDPR Mode does not cover](#what-gdpr-mode-does-not-cover) below. ### What GDPR Mode Disables [#what-gdpr-mode-disables] | Data | GDPR mode off (default) | GDPR mode on | | ------------------------------- | ----------------------- | --------------------- | | Call recording (audio file) | ✅ Stored | ❌ Not recorded | | Transcript | ✅ Stored | ❌ Not stored | | Call summary | ✅ Stored | ❌ Not generated | | Conversation history (messages) | ✅ Stored | ❌ Not stored | | Webhook: transcript | ✅ Included | ❌ Omitted | | Webhook: recording URL | ✅ Included | ❌ Omitted | | Webhook: summary | ✅ Included | ❌ Omitted | | Call record (metadata) | ✅ Stored | ✅ Stored (no content) | | Usage & billing | ✅ Tracked | ✅ Tracked | | Duration | ✅ Stored | ✅ Stored | Call metadata (duration, timestamp, phone numbers, end reason) is always stored for billing purposes. Only the content of the conversation is omitted. ### What GDPR Mode does not cover [#what-gdpr-mode-does-not-cover] GDPR Mode controls what **VoiceDock** stores on its own infrastructure: with it on, we keep no conversation content, only billing metadata. It does not control what the **AI providers** that handle the call in real time do with that audio and text. Those are separate parties with their own terms, and what each retains differs per provider and per configuration. We no longer summarise their terms here, because a summary we maintain is a summary that goes stale without anyone noticing. What each sub-processor does with call data, including retention, training and transfer basis, is stated per provider in the [VoiceDock Trust Center](https://trust.voicedock.ai). If you need to know what your own assistant's providers currently do, ask us and we will tell you per component. ### Webhook Behavior in GDPR Mode [#webhook-behavior-in-gdpr-mode] The `end-of-call-report` webhook is still sent, but with a minimal payload. The `transcript`, `summary`, `messages`, and `recording_url` fields are excluded. A `gdpr_mode: true` flag is included so your webhook handler can identify these calls. ```json { "message": { "type": "end-of-call-report", "gdpr_mode": true, "call": { "id": "...", "duration": 42, "ended_reason": "user_hangup" } } } ``` ### Configuration [#configuration-1] Enable GDPR mode on an assistant: ```json { "gdpr_mode": true } ``` Via API: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/{id} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"gdpr_mode": true}' ``` GDPR mode can also be applied dynamically per call via the `assistant-request` webhook response, allowing you to enable it based on the caller's number or other context. *** ## Using Both Together [#using-both-together] Recording consent and GDPR mode complement each other and can be enabled simultaneously: * **Recording consent** ensures the caller actively agrees before any processing begins * **GDPR mode** ensures no conversation content is stored after the call This combination is suitable for scenarios where you need to demonstrate consent was requested but do not want to retain any call content — for example, informational hotlines, internal tooling, or deployments in jurisdictions with strict data minimization requirements. ```json { "recording_consent": { "enabled": true, "message": "This call is processed by an AI assistant. No recording will be stored. Press 1 to continue, or press 2 to hang up." }, "gdpr_mode": true } ``` > \*\*Tip: \*\* When combining both features, you may want to update your consent message to reflect that no recording is stored — as shown in the example above. *** ## Comparison [#comparison] | Feature | Recording Consent | GDPR Mode | | ----------------------------------------- | ----------------- | --------- | | Requires caller action | ✅ Yes (press 1) | ❌ No | | Prevents data processing before agreement | ✅ Yes | ❌ No | | Disables recording storage | ❌ No | ✅ Yes | | Disables transcript storage | ❌ No | ✅ Yes | | Disables summary generation | ❌ No | ✅ Yes | | Affects webhook payload | ❌ No | ✅ Yes | | Applies to web calls | ❌ No (phone only) | ✅ Yes | | Configurable per assistant | ✅ Yes | ✅ Yes | | Overridable per call (assistant-request) | ❌ No | ✅ Yes | *** ## Related Documentation [#related-documentation] * [VoiceDock Trust Center](https://trust.voicedock.ai) — sub-processors, retention, transfer basis and the Data Processing Agreement * [EU-resident configuration](/docs/platform/eu-data-sovereignty) — how to configure an assistant that processes only in the EU * [Data Processing Agreement](/docs/guides/data-processing-agreement) — how to obtain it * [VoiceDock, Flireo & HMS Sovereign](/docs/platform/about) — How our company, product, and technology relate * [Assistant Configuration](/docs/core-concepts/assistants) — Full API reference for assistant settings * [Webhooks](/docs/webhooks/overview) — Webhook events and payloads including GDPR mode behavior # Voicemail Detection Source: https://doc.voicedock.ai/docs/features/voicemail-detection > Technical documentation for the `voicemail_detection` feature. ## Overview [#overview] Voicemail Detection is a feature for **outbound calls only** that allows the AI assistant to detect when it has reached a voicemail system instead of a live person. When voicemail is detected, the assistant can either: 1. Leave a pre-configured voicemail message, or 2. Hang up immediately without leaving a message This prevents wasted assistant time talking to voicemail systems and enables targeted voicemail campaigns. ## Configuration [#configuration] ### Assistant Schema [#assistant-schema] ```json { "voicemail_detection": true, "voicemail_message": "Hello, this is a message from Acme Corp. Please call us back at 555-1234. Thank you." } ``` | Field | Type | Default | Description | | --------------------- | ------- | ------- | ------------------------------------------------------------------------------------------ | | `voicemail_detection` | boolean | `false` | Enable/disable voicemail detection | | `voicemail_message` | string | `null` | Message to leave on voicemail. If null/empty, assistant hangs up without leaving a message | ## How It Works [#how-it-works] ### Detection Method [#detection-method] Voicemail detection uses **speech-to-text analysis** of the first utterance from the called party. When the recipient answers, their greeting is transcribed and analyzed for voicemail indicators. ### Voicemail Indicators [#voicemail-indicators] The system checks for these keywords/phrases in the initial greeting: ```python voicemail_indicators = [ "voicemail", "leave a message", "please record", "unavailable", "not available", "not here", "after the tone", "after the beep", "press star", "speak your message", "call back later", "mailbox", "beep", "message" ] ``` Supports multiple languages including English and Dutch. ### Detection Flow [#detection-flow] ``` [Outbound Call] → [Recipient Answers] → [First Utterance] → [STT Transcription] ↓ [Check for Voicemail Indicators] ↓ ↓ [No Match] [Match Found] ↓ ↓ [Continue Call] [Voicemail Detected] ↓ [Has voicemail_message?] ↓ ↓ [No] [Yes] ↓ ↓ [Hang Up] [Leave Message → Hang Up] ``` ### One-Time Check [#one-time-check] Detection only occurs on the **first user utterance**. Once the check is done (match or no match), voicemail detection is complete for that call. This prevents false positives during normal conversation. ## Behavior [#behavior] 1. **User speaks** - Recipient answers and speaks first 2. **First utterance check** - System checks if this is the first time they spoke 3. **Keyword scan** - Transcription is analyzed for voicemail indicators 4. **Detection triggered** - If match found, voicemail is detected 5. **Leave message** - If `voicemail_message` is set, assistant speaks it 6. **Wait for playback** - System waits for message to finish playing 7. **End call** - Call is automatically terminated ### Message Playback [#message-playback] The system automatically waits for the voicemail message to finish playing before ending the call. The timing is estimated based on message length and typical speech rate. ## Use Cases [#use-cases] ### Campaign Calls [#campaign-calls] Perfect for outbound campaigns where you want to: * Leave consistent voicemail messages * Track voicemail vs. live answer rates * Avoid wasting assistant resources on voicemail systems ### Appointment Reminders [#appointment-reminders] * Leave reminder message if voicemail reached * Transfer to booking system if live person answers ### Sales Outreach [#sales-outreach] * Leave callback request on voicemail * Engage in conversation if live person answers ## Limitations [#limitations] ### Detection Accuracy [#detection-accuracy] * **Not 100% accurate**: Relies on keyword matching in transcribed speech * **Language dependent**: Indicators are primarily English/Dutch * **False negatives**: Some voicemail greetings may not contain keywords * **False positives**: Rare, but possible if person says "leave a message" naturally ### Speech-Only Detection [#speech-only-detection] The system does **not** detect the voicemail beep tone. It relies solely on speech analysis of the greeting. This means: * Detection works by analyzing the spoken greeting * Some voicemail systems with unusual greetings may not be detected ### Outbound Only [#outbound-only] Voicemail detection is **only available for outbound calls**. The feature is automatically disabled for inbound calls. ## Webhook Events [#webhook-events] ### Status Update [#status-update] When voicemail is detected: ```json { "message": { "type": "status-update", "call": { "status": "in-progress" } } } ``` Note: There is no special "voicemail-detected" status. The call remains "in-progress" until it ends. ### End of Call Report [#end-of-call-report] ```json { "message": { "type": "end-of-call-report", "call": { "status": "ended" }, "summary": "Voicemail detected. Left message: Hello, this is...", "messages": [ {"role": "user", "content": "You have reached the voicemail of..."}, {"role": "assistant", "content": "Hello, this is a message from..."} ] } } ``` The transcript will show the voicemail greeting (user) and the message left (assistant). ## Configuration Examples [#configuration-examples] ### Leave Voicemail Message [#leave-voicemail-message] ```json { "voicemail_detection": true, "voicemail_message": "Hello, this is Sarah from Acme Corp calling about your recent inquiry. Please call us back at 555-1234 at your earliest convenience. Thank you and have a great day." } ``` ### Hang Up Without Message [#hang-up-without-message] ```json { "voicemail_detection": true, "voicemail_message": null } ``` Or simply omit `voicemail_message`: ```json { "voicemail_detection": true } ``` ### Disabled (Default) [#disabled-default] ```json { "voicemail_detection": false } ``` ## Interaction with Other Features [#interaction-with-other-features] ### With `autonomous_silence_handling` [#with-autonomous_silence_handling] Both can be enabled. Voicemail detection happens first (on initial greeting). If voicemail is not detected, silence handling activates during normal conversation. ### With `first_message` [#with-first_message] For outbound calls, the assistant does **not** speak first - it waits for the recipient to answer and speak. This is intentional: * Allows voicemail detection to analyze the greeting * More natural for outbound calls (recipient says "hello" first) ### With Campaign Calls [#with-campaign-calls] When using campaigns with lead context: * `voicemail_detection` and `voicemail_message` come from the assistant config * Lead context is injected into the system prompt for live conversations * Voicemail message is static (not personalized with lead data) ## Troubleshooting [#troubleshooting] ### Voicemail Not Being Detected [#voicemail-not-being-detected] 1. Verify `voicemail_detection: true` is set in assistant configuration 2. Check if the voicemail greeting contains any of the indicator keywords listed above 3. Review the call transcript to see what was detected in the first utterance 4. Consider that some voicemail systems may use greetings without standard indicators ### Message Not Being Left [#message-not-being-left] 1. Verify `voicemail_message` is set and not empty in assistant configuration 2. Ensure your TTS configuration is working correctly 3. Check the call transcript to confirm voicemail was detected ### Call Not Ending After Voicemail [#call-not-ending-after-voicemail] 1. Review the end-of-call-report webhook for error details 2. Verify the assistant has proper permissions and configuration 3. Check that the call didn't exceed `max_duration_seconds` separately ## Future Improvements [#future-improvements] Potential enhancements (not currently implemented): 1. **Beep detection**: Use audio analysis to detect voicemail tone 2. **Configurable indicators**: Allow custom keywords per assistant/language 3. **Personalized voicemail**: Template variables in `voicemail_message` 4. **Voicemail status**: Dedicated call status for voicemail detection 5. **Retry logic**: Option to call back later if voicemail reached # Web Calls Source: https://doc.voicedock.ai/docs/features/web-calls Web calls let your users speak directly to an AI assistant from their browser — no phone number required. The browser connects via WebRTC and the assistant runs exactly like it does for phone calls. > **Note**: We're working on official SDKs for React, Vue, and vanilla JavaScript. In the meantime, this guide provides everything you need to build a working integration using the open-source `livekit-client` and `@livekit/components-react` packages. ## How It Works [#how-it-works] ``` User clicks "Talk to AI" ↓ Your backend → POST https://assistant-api.hmsovereign.com/v1/web-calls (Bearer ) ↓ HMS validates key, creates voice room, dispatches assistant → returns { token, server_url } ↓ Your backend passes token + server_url to the browser ↓ Browser connects via WebRTC (microphone audio) using the token ↓ AI assistant picks up → full STT → LLM → TTS pipeline ↓ Call ends → summary, transcript, credits deducted, webhook fired ``` The call appears in your **Calls** dashboard with `direction: "web"` and is billed at the same per-minute rate as phone calls. > **WarningSecurity**: **Never expose your API key to the browser.** Always proxy web call requests through your own backend server. The browser only ever receives the short-lived token. *** ## API Reference [#api-reference] ### Create Web Call [#create-web-call] ``` POST https://assistant-api.hmsovereign.com/v1/web-calls ``` #### Authentication [#authentication] | Header | Value | | --------------- | --------------------- | | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | Your organization API key is found in the HMS Sovereign dashboard under **Developer > Your API Key**. The `org_id` is automatically derived from the key — you do not need to pass it. #### Request Body [#request-body] | Field | Type | Required | Description | | -------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------ | | `assistant_id` | `string (uuid)` | No\* | Saved assistant to use. Must belong to your organization. | | `assistant_override` | `object` | No | Partial field overrides applied on top of `assistant_id` (hybrid mode). Requires `assistant_id`. | | `assistant` | `object` | No\* | Full inline assistant config (transient mode). Cannot be combined with `assistant_override`. | \*At least one of `assistant_id` or `assistant` is required. See [configuration modes](#configuration-modes) below. **Reference mode** — use a saved assistant as-is: ```json { "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88" } ``` **Hybrid mode** — saved assistant with partial overrides: ```json { "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "assistant_override": { "first_message": "Custom greeting!", "llm_config": { "messages": [{ "role": "system", "content": "You are a sales assistant." }] } } } ``` **Transient mode** — full inline config, no saved assistant required: ```json { "assistant": { "stt_config": { "provider": "deepgram", "model": "nova-3", "language": "en" }, "llm_config": { "provider": "openai", "model": "gpt-4.1-mini", "messages": [{ "role": "system", "content": "You are a helpful assistant." }] }, "tts_config": { "provider": "elevenlabs", "voice_id": "your-voice-id" }, "first_message": "Hello! How can I help you?" } } ``` **Available fields in `assistant` / `assistant_override`:** `stt_config`, `llm_config`, `tts_config`, `first_message`, `business_name`, `name`, `analysis_plan`, `autonomous_silence_handling`, `gdpr_mode`, `webhook_url`, `webhook_secret`, `webhook_events`, `metadata`. Transient mode requires `stt_config`, `llm_config`, and `tts_config` inside `assistant`. *** ## Passing Custom Metadata [#passing-custom-metadata] Use the `metadata` field inside `assistant_override` (or `assistant` in transient mode) to attach arbitrary key-value data to a web call. HMS Sovereign passes it through unchanged to all webhook payloads under `message.assistant.metadata`. This is the correct way to correlate a web call with your own users, sessions, or records — for example, passing a `user_id` so your webhook handler knows which user the call belongs to. > **Note**: `assistant-request` does **not** fire for web calls. Unlike inbound phone calls — where you can inject metadata dynamically in the `assistant-request` response — web calls have no pre-call webhook. All metadata must be passed at session creation time. ```json { "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "assistant_override": { "metadata": { "user_id": "usr_8f3a2b1c", "session_id": "ses_9d4e3c2b", "plan": "pro" } } } ``` The `metadata` object appears as-is in every webhook fired for that call: ```json { "message": { "type": "end-of-call-report", "call": { "id": "3f2a1b4c-...", "type": "web_call" }, "assistant": { "metadata": { "user_id": "usr_8f3a2b1c", "session_id": "ses_9d4e3c2b", "plan": "pro" } } } } ``` > **WarningKey casing is preserved**: Metadata keys are passed through exactly as you send them — HMS Sovereign does not convert them to snake\_case or any other format. If your webhook handler expects `user_id`, send `user_id`. If it expects `UserID`, send `UserID`. #### Configuration Modes [#configuration-modes] | Mode | When to use | | ------------- | -------------------------------------------------------------------------------------------------------------- | | **Reference** | Use a saved assistant exactly as configured in the dashboard | | **Hybrid** | Use a saved assistant but override specific fields per-call (e.g. dynamic first message, custom system prompt) | | **Transient** | Fully define the assistant inline — useful for dynamic or ephemeral assistants not saved in the dashboard | #### Success Response — `200 OK` [#success-response--200-ok] ```json { "success": true, "call_id": "3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c", "room_name": "web-3f2a1b4c", "token": "", "server_url": "wss://rtc.hmsovereign.com" } ``` | Field | Type | Description | | ------------ | --------------- | -------------------------------------------------------------------- | | `success` | `boolean` | Always `true` on 200 | | `call_id` | `string (uuid)` | Unique call ID — appears in your calls dashboard | | `room_name` | `string` | Voice room name | | `token` | `string` | Short-lived JWT (5-minute TTL) — pass this to the browser to connect | | `server_url` | `string` | WebSocket URL for the voice server (always `wss://`) | **Important:** the token expires after **5 minutes** whether or not the user joins. Once the call starts, it runs until the user hangs up or the 5-minute max duration is hit. #### Error Responses [#error-responses] | Status | Detail | Meaning | | ------ | ---------------------------------------------------------------------- | ------------------------------------------ | | `400` | `Must provide assistant_id, assistant, or both` | No configuration provided | | `400` | `assistant_override requires assistant_id` | Override provided without a base assistant | | `400` | `Cannot provide both assistant and assistant_override` | Ambiguous config mode | | `400` | `Transient assistant must provide: stt_config, llm_config, tts_config` | Transient mode missing required configs | | `401` | `Invalid API key` | API key not recognized | | `402` | `Insufficient credits to start web call` | Organization has no balance | | `403` | `Assistant not found or does not belong to this organization` | Invalid `assistant_id` for this org | | `429` | `Maximum 3 concurrent web calls per organization` | Too many active calls — user must wait | | `500` | `Failed to create web call session: ...` | Server error | *** ## Limits [#limits] | Limit | Value | | ----------------------------------------- | -------------- | | Max concurrent web calls per organization | **3** | | Max call duration | **5 minutes** | | Token TTL (time to join) | **5 minutes** | | Room auto-deleted if nobody joins | **60 seconds** | *** ## Integration Guide [#integration-guide] ### Set Up Your Backend [#set-up-your-backend] Create an endpoint that proxies requests to the HMS Sovereign API. This keeps your API key secure on the server — the browser only ever receives the short-lived token. #### Node.js / Express [#nodejs--express] ```javascript const express = require("express"); const app = express(); app.use(express.json()); const HMS_API_KEY = process.env.HMS_API_KEY; // org API key — never expose to browser const HMS_ASSISTANT_ID = process.env.HMS_ASSISTANT_ID; // your assistant UUID app.post("/api/start-call", async (req, res) => { try { // Reference mode (most common): const body = { assistant_id: HMS_ASSISTANT_ID }; // Hybrid mode — override specific fields per-call: // const body = { // assistant_id: HMS_ASSISTANT_ID, // assistant_override: { first_message: "Welcome! How can I help?", llm_config: { ... } } // }; // Transient mode — full inline config, no saved assistant: // const body = { // assistant: { // stt_config: { provider: "deepgram", model: "nova-3", language: "en" }, // llm_config: { provider: "openai", model: "gpt-4.1-mini", messages: [...] }, // tts_config: { provider: "elevenlabs", voice_id: "..." }, // first_message: "Hello!" // } // }; const response = await fetch("https://assistant-api.hmsovereign.com/v1/web-calls", { method: "POST", headers: { "Authorization": `Bearer ${HMS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); const data = await response.json(); if (!response.ok) { return res.status(response.status).json({ error: data.detail }); } // Only pass token and server_url to the browser — never the API key res.json({ token: data.token, server_url: data.server_url }); } catch (err) { res.status(500).json({ error: "Failed to start call" }); } }); ``` #### Python / FastAPI [#python--fastapi] ```python import os, httpx from fastapi import FastAPI, HTTPException app = FastAPI() HMS_API_KEY = os.environ["HMS_API_KEY"] # org API key — never expose to browser HMS_ASSISTANT_ID = os.environ["HMS_ASSISTANT_ID"] # your assistant UUID @app.post("/api/start-call") async def start_call(): # Reference mode (most common): body = {"assistant_id": HMS_ASSISTANT_ID} # Hybrid mode — override specific fields per-call: # body = {"assistant_id": HMS_ASSISTANT_ID, "assistant_override": {"first_message": "Welcome!"}} # Transient mode — full inline config: # body = {"assistant": {"stt_config": {...}, "llm_config": {...}, "tts_config": {...}}} async with httpx.AsyncClient() as client: r = await client.post( "https://assistant-api.hmsovereign.com/v1/web-calls", headers={"Authorization": f"Bearer {HMS_API_KEY}"}, json=body, ) if r.status_code != 200: raise HTTPException( status_code=r.status_code, detail=r.json().get("detail") ) data = r.json() # Only pass token and server_url to the browser — never the API key return {"token": data["token"], "server_url": data["server_url"]} ``` #### Next.js API Route [#nextjs-api-route] ```typescript // app/api/start-call/route.ts import { NextResponse } from "next/server"; const HMS_API_KEY = process.env.HMS_API_KEY!; // org API key — never expose to browser const HMS_ASSISTANT_ID = process.env.HMS_ASSISTANT_ID!; // your assistant UUID export async function POST() { // Reference mode (most common): const body = { assistant_id: HMS_ASSISTANT_ID }; // Hybrid mode — override specific fields per-call: // const body = { // assistant_id: HMS_ASSISTANT_ID, // assistant_override: { first_message: "Welcome!", llm_config: { ... } } // }; // Transient mode — full inline config: // const body = { // assistant: { // stt_config: { provider: "deepgram", model: "nova-3", language: "en" }, // llm_config: { provider: "openai", model: "gpt-4.1-mini", messages: [...] }, // tts_config: { provider: "elevenlabs", voice_id: "..." }, // first_message: "Hello!" // } // }; const response = await fetch("https://assistant-api.hmsovereign.com/v1/web-calls", { method: "POST", headers: { "Authorization": `Bearer ${HMS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); const data = await response.json(); if (!response.ok) { return NextResponse.json({ error: data.detail }, { status: response.status }); } // Only pass token and server_url to the browser — never the API key return NextResponse.json({ token: data.token, server_url: data.server_url }); } ``` ### Install the Client SDK [#install-the-client-sdk] Install `livekit-client` in your frontend project to connect to the voice session: #### npm [#npm] ```bash npm install livekit-client ``` #### yarn [#yarn] ```bash yarn add livekit-client ``` #### pnpm [#pnpm] ```bash pnpm add livekit-client ``` For React projects, also install the React components library: ```bash npm install @livekit/components-react @livekit/components-styles ``` ### Connect to the Voice Session [#connect-to-the-voice-session] Fetch the token from your backend and connect to the voice room. #### Vanilla JavaScript [#vanilla-javascript] ```javascript import { Room, RoomEvent } from "livekit-client"; async function startCall() { // 1. Get token from your backend const res = await fetch("/api/start-call", { method: "POST" }); const { token, server_url } = await res.json(); // 2. Connect to the voice room const room = new Room(); await room.connect(server_url, token); // 3. Enable microphone await room.localParticipant.setMicrophoneEnabled(true); // 4. Play assistant audio room.on(RoomEvent.TrackSubscribed, (track) => { if (track.kind === "audio") { const element = track.attach(); document.body.appendChild(element); } }); // 5. Handle disconnect room.on(RoomEvent.Disconnected, () => { console.log("Call ended"); }); return room; } function endCall(room) { room.disconnect(); } ``` #### React [#react] ```tsx "use client"; import { useState, useCallback } from "react"; import { LiveKitRoom, useVoiceAssistant, RoomAudioRenderer, } from "@livekit/components-react"; import "@livekit/components-styles"; interface CallSession { token: string; server_url: string; } export function VoiceCallButton({ agentId }: { agentId: string }) { const [session, setSession] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const startCall = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/start-call", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assistant_id: agentId }), }); const data = await res.json(); if (!res.ok) { const msg = res.status === 429 ? "Too many active calls — please try again shortly." : res.status === 402 ? "Insufficient credits." : data.detail ?? "Failed to start call."; setError(msg); return; } setSession(data); } catch { setError("Network error — please try again."); } finally { setLoading(false); } }, [agentId]); const endCall = useCallback(() => setSession(null), []); if (session) { return ( ); } return (
{error &&

{error}

}
); } function CallInterface({ onHangUp }: { onHangUp: () => void }) { const { state } = useVoiceAssistant(); return (

{state === "connecting" && "Connecting..."} {state === "listening" && "Assistant is listening"} {state === "thinking" && "Assistant is thinking..."} {state === "speaking" && "Assistant is speaking"}

); } ``` *** ## Live Transcription [#live-transcription] The assistant automatically publishes real-time transcriptions for both user speech (STT output) and assistant speech (synchronized with TTS playback) via the `lk.transcription` text stream topic. This is **enabled by default** — no configuration needed. ### How It Works [#how-it-works-1] | Source | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **User speech** | The assistant runs STT and publishes the recognized text. Interim results arrive first (`lk.transcription_final: "false"`), followed by the final result (`"true"`). | | **Assistant speech** | The assistant's text is synchronized word-by-word with audio playback. If the assistant is interrupted, the transcription is truncated to match what was actually spoken. | Each speech segment has a unique `lk.segment_id`. Interim and final results share the same ID, so you can replace interim entries with the final version. ### Vanilla JavaScript [#vanilla-javascript-1] ```javascript room.registerTextStreamHandler("lk.transcription", async (reader, participantInfo) => { const text = await reader.readAll(); const attrs = reader.info.attributes; const isFinal = attrs["lk.transcription_final"] === "true"; const segmentId = attrs["lk.segment_id"] ?? reader.info.id; // participantInfo.identity is the actual speaker — // user speech is published with the user's identity, assistant speech with the assistant's identity const isUser = participantInfo.identity === room.localParticipant.identity; const role = isUser ? "user" : "assistant"; console.log(`[${role}] ${text}`, { isFinal, segmentId }); }); ``` ### React Example [#react-example] Use `useRoomContext` from `@livekit/components-react` inside a `` to access the room: ```tsx "use client"; import { useEffect, useRef, useState } from "react"; import { useRoomContext } from "@livekit/components-react"; interface TranscriptEntry { id: string; role: "user" | "assistant"; text: string; isFinal: boolean; } export function LiveTranscript() { const room = useRoomContext(); const [entries, setEntries] = useState([]); const scrollRef = useRef(null); useEffect(() => { const unregister = room.registerTextStreamHandler( "lk.transcription", async (reader, participantInfo) => { const text = await reader.readAll(); const attrs = reader.info.attributes; const isFinal = attrs["lk.transcription_final"] === "true"; const segmentId = attrs["lk.segment_id"] ?? reader.info.id; const isUser = participantInfo.identity === room.localParticipant.identity; const role = isUser ? "user" : "assistant"; setEntries((prev) => { const existing = prev.findIndex((e) => e.id === segmentId); const entry: TranscriptEntry = { id: segmentId, role, text, isFinal }; if (existing >= 0) { const updated = [...prev]; updated[existing] = entry; return updated; } return [...prev, entry]; }); } ); return () => { unregister?.(); }; }, [room]); useEffect(() => { scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight); }, [entries]); return (
{entries.map((entry) => (
{entry.role === "assistant" ? "Assistant" : "You"}: {entry.text}
))}
); } ``` Place `` inside your `` component so it has access to the room context: ```tsx ``` > **Note**: Tool/function calls are not published over the transcription stream. They appear in the post-call transcript via webhooks only. *** ## Handling Microphone Permissions [#handling-microphone-permissions] The browser will prompt for microphone access when connecting with `audio={true}`. If the user denies permission, a `MediaDeviceFailure` error is raised on the `onError` callback: ```tsx { // MediaDeviceFailure is thrown when microphone access is denied setError("Microphone access was denied. Please allow microphone access and try again."); endCall(); }} > ``` *** ## Webhooks [#webhooks] Web calls fire the same webhook events as phone calls. The `call.type` field is `"web_call"` instead of `"inbound_phone_call"`. ### `status-update` — call started [#status-update--call-started] ```json { "message": { "type": "status-update", "status": "in-progress", "call": { "id": "3f2a1b4c-...", "type": "web_call", "status": "in-progress" } } } ``` ### `end-of-call-report` [#end-of-call-report] ```json { "message": { "type": "end-of-call-report", "call": { "id": "3f2a1b4c-...", "type": "web_call", "status": "ended" }, "end_reason": "user_hangup", "duration_seconds": 47, "summary": "The user asked about pricing...", "messages": [ "..." ], "assistant": { "metadata": { "..." } } } } ``` ### End Reasons [#end-reasons] | Value | Meaning | | -------------- | -------------------------------------------- | | `user_hangup` | Browser disconnected or user clicked hang up | | `agent_hangup` | Assistant called the `end_call` tool | | `max_duration` | 5-minute hard limit reached | | `error` | Unexpected assistant error | | `config_error` | Assistant configuration is invalid | See [Webhooks Overview](/docs/webhooks/overview) for webhook setup and configuration. *** ## Billing [#billing] Web calls are billed at the same per-minute rate as phone calls. Usage appears in your dashboard under the **Calls** section with `direction: "web"`. *** ## FAQ [#faq] #### Do I need to run my own voice infrastructure? [#do-i-need-to-run-my-own-voice-infrastructure] No. HMS Sovereign provides fully managed voice infrastructure. Your users connect to our servers using the short-lived token returned by the API. #### Can I use this on mobile browsers? [#can-i-use-this-on-mobile-browsers] Yes. Web calls work on any browser that supports WebRTC, including mobile Safari and Chrome on iOS/Android. #### What happens if the user loses internet connection? [#what-happens-if-the-user-loses-internet-connection] The call ends automatically. A disconnection is detected server-side and triggers the `end-of-call-report` webhook with `end_reason: "error"`. #### Can I customize the assistant per-call? [#can-i-customize-the-assistant-per-call] Yes. Use hybrid mode to override specific fields (e.g. first message, system prompt) on top of a saved assistant, or use transient mode to define the full assistant configuration inline — no saved assistant required. See the [configuration modes](#configuration-modes) section above. #### Are official SDKs coming? [#are-official-sdks-coming] Yes — we're actively building first-party SDKs for React, Vue, and vanilla JavaScript. This guide will be updated when they're available. *** ## Next Steps [#next-steps] * [Configure webhooks](/docs/webhooks/overview) to receive real-time call events * [Add custom tools](/docs/configuration/custom-tools) to let your assistant access external data * [Set up call analysis](/docs/features/call-analysis) for structured post-call data # Workflow definitions Source: https://doc.voicedock.ai/docs/features/workflow-definitions This page is the field-by-field reference for the workflow definition — the JSON graph you save via the [Workflows API](/docs/api/workflows/createWorkflow) or build visually in the dashboard. For the concept and how workflows interact with assistants, start at [Workflows](/docs/features/workflows). ## Top level [#top-level] | Field | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------- | | `version` | integer | Yes | Contract version. Currently always `1`. | | `entry_node` | string | Yes | Node id where every call starts. Must be a conversation node. | | `global_prompt` | string | No | Prepended to the instructions of every conversation node. | | `nodes` | array | Yes | The steps. At least one node. | | `edges` | array | No | The transitions between steps. | Node ids are lowercase: letters, digits and underscores, starting with a letter (`^[a-z][a-z0-9_]{0,40}$`), and must be unique. ## Conversation node [#conversation-node] The talking step. The entry node is always a conversation node. | Field | Type | Required | Description | | -------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | Unique node id | | `type` | string | Yes | `conversation` | | `instructions` | string | Yes | System prompt for this step (after the `global_prompt`) | | `first_line` | string | No | Fixed line spoken when the step becomes active. On realtime models it is passed as an opening hint instead of spoken verbatim. | | `global` | boolean | No | Reachable from every conversation node without edges | | `global_description` | string | If `global` | When a caller should be brought here | | `llm_config` | object | No | Per-step model override: `provider`, `model`, `temperature` | | `voice` | string | No | Voice swap within the assistant's provider (realtime: the speech-to-speech voice; pipeline: the TTS voice). Mutually exclusive with `tts_config`. | | `tts_config` | object | No | Full text-to-speech override incl. provider parameters (pipeline only) | | `stt_config` | object | No | Speech-recognition override: `provider`, `model`, `language`, `keyterms` (pipeline only) | | `tools` | array | No | Tools available in this step, same format as assistant tools | | `ui` | object | No | Canvas position (`x`, `y`). Ignored by the calling engine. | **Global nodes.** A global conversation node gets an implicit transition from every other conversation node, described by its `global_description`. An explicit edge to the same node takes precedence. Global nodes also count as reachability roots — a graph section only reachable via a global node is valid. **Per-step models.** `llm_config` is allowed on conversation nodes only, and requires a pipeline (non-realtime) assistant on the number. All overrides are constructed once at call start, fail-closed: an unknown model rejects the call before pickup instead of failing mid-conversation. The entry node's override applies to the whole session. **Per-step voices and speech.** `voice` works on both modes as a voice swap within the assistant's provider (on realtime assistants a new speech connection with the other voice is prepared per step). `tts_config` and `stt_config` are full replacements of the assistant's speech configs for that step — replace, not merge, so a provider switch never carries half the old provider's parameters. Both are pipeline-only and reject on realtime assistants with a developer-readable reason; the entry node's overrides apply to the whole session. All of it prebuilds fail-closed at call start. ## Tool node [#tool-node] A background step: the platform calls the webhook itself and advances automatically. | Field | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------- | | `id` | string | Yes | Unique node id | | `type` | string | Yes | `tool` | | `tool_name` | string | Yes | Name sent in the [tool-calls webhook](/docs/webhooks/tool-calls) | | `url` | string | No | Per-node URL override; defaults to the webhook resolved for the call | | `arguments` | object | No | Static arguments sent with the call | | `first_line` | string | No | Hold line spoken while the call runs | | `ui` | object | No | Canvas position. Ignored by the engine. | The result (or the error, if the call fails) is placed in the conversation context, and the flow advances along the node's single outgoing edge either way — the next step can see and react to what happened. For arguments the assistant should collect from the caller during the conversation, use a regular tool on a conversation node; tool-node arguments are static. ## Transfer node [#transfer-node] Hands the call to a human. Terminal. | Field | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------- | | `id` | string | Yes | Unique node id | | `type` | string | Yes | `transfer` | | `destination` | string | Yes | E.164 phone number (`+31612345678`) | | `first_line` | string | No | Announcement spoken once before dialing | | `ui` | object | No | Canvas position. Ignored by the engine. | The platform speaks the announcement and starts the transfer as soon as the node becomes active — the action is executed in code and cannot be forgotten or repeated. The number needs a SIP trunk that allows transfers. ## End node [#end-node] Ends the call politely. Terminal. | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------- | | `id` | string | Yes | Unique node id | | `type` | string | Yes | `end` | | `first_line` | string | No | Goodbye line; defaults to a neutral thank-you | | `ui` | object | No | Canvas position. Ignored by the engine. | ## Edges [#edges] | Field | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------------------- | | `from` | string | Yes | Source node id (conversation or tool nodes only) | | `to` | string | Yes | Target node id | | `description` | string | Yes\* | When the assistant should take this transition. *Optional on edges leaving a tool node.* | | `message` | string | No | Line spoken during the hand-over | Write descriptions as observable conditions — *"The caller asks about opening hours"* — not instructions. The assistant reads them to decide where the conversation goes next. ## Validation rules [#validation-rules] Definitions are validated when you save them (API and dashboard alike) and again, fail-closed, when a call comes in. The full rule set: * `version` must be `1`; `nodes` must be a non-empty array. * Node ids match `^[a-z][a-z0-9_]{0,40}$` and are unique. * `entry_node` references an existing **conversation** node. * Conversation nodes require non-empty `instructions`. * Transfer nodes require a valid E.164 `destination`. * Tool nodes require a non-empty `tool_name` and **exactly one** outgoing edge. * End and transfer nodes cannot have outgoing edges. * Edge endpoints must exist; no self-loops; no duplicate `from`/`to` pairs. * Edge `description` is required, except on edges leaving a tool node. * `global` is allowed on conversation nodes only and requires a `global_description`. * `llm_config`, `voice`, `tts_config` and `stt_config` are allowed on conversation nodes only; `voice` must be a non-empty string, `tts_config`/`stt_config` must be objects, and `voice` and `tts_config` are mutually exclusive. * A node's own tool names must not collide with the generated transition tools (`go_to_`). * Every node must be reachable from `entry_node` (global nodes count as extra roots). Violations are returned as a `details` array, one specific message per problem: ```json { "error": "Invalid workflow definition", "details": [ "node 'human': transfer nodes require destination in E.164 format (e.g. +31612345678)", "node 'lookup': tool nodes require exactly one outgoing edge (got 0)" ] } ``` ## Complete example [#complete-example] A dental practice reception: general questions at the reception, an availability lookup as a background step, a scheduling specialist on a stronger model, a human transfer on request, and a clean goodbye. The reception is global, so "put me back to the reception" works from anywhere. ```json { "version": 1, "entry_node": "reception", "global_prompt": "You work for Riverside Dental. Be warm and concise.", "nodes": [ { "id": "reception", "type": "conversation", "instructions": "You are the receptionist. Handle general questions about the practice. When the caller asks about availability, hand off to the availability check.", "first_line": "Good afternoon, Riverside Dental. How can I help you?", "global": true, "global_description": "The caller wants the reception back or has a general question." }, { "id": "check_availability", "type": "tool", "tool_name": "check_availability", "url": "https://api.example.com/availability", "arguments": { "location": "riverside", "days_ahead": 7 }, "first_line": "One moment while I look that up for you." }, { "id": "specialist", "type": "conversation", "instructions": "You are Sam, the scheduling specialist. Use the availability result from the context to help the caller pick a slot.", "first_line": "You are speaking with Sam, the scheduling specialist.", "llm_config": { "provider": "openai", "model": "gpt-5.4-mini" } }, { "id": "human", "type": "transfer", "destination": "+31201234567", "first_line": "I am transferring you to a colleague now, one moment please." }, { "id": "goodbye", "type": "end", "first_line": "Thanks for calling Riverside Dental. Have a great day!" } ], "edges": [ { "from": "reception", "to": "check_availability", "description": "The caller asks about available appointment slots.", "message": "Let me check that for you." }, { "from": "check_availability", "to": "specialist" }, { "from": "specialist", "to": "human", "description": "The caller explicitly asks for a human employee." }, { "from": "reception", "to": "goodbye", "description": "The caller indicates the conversation is finished." }, { "from": "specialist", "to": "goodbye", "description": "The caller indicates the conversation is finished." } ] } ``` # Workflows Source: https://doc.voicedock.ai/docs/features/workflows Workflows turn a single assistant prompt into a structured, multi-step call flow. Instead of one long system prompt that has to cover every situation, you draw the conversation as a graph: a reception step, a lookup step, a specialist step, a transfer to a human, a polite goodbye. Each step has its own focused instructions, and the assistant moves between steps based on transition rules you define. Workflows are thoroughly tested and ready to use. They carry a **Beta** label until they have proven themselves at high call volumes and in rare edge cases. ## When to use a workflow [#when-to-use-a-workflow] A single well-written assistant handles most calls fine. Reach for a workflow when: * **The call has distinct phases** — intake, verification, scheduling, hand-off — and one prompt trying to do all of them gets muddled. * **You need a guaranteed action mid-call** — look something up, register a lead — without trusting the model to remember to call a tool. * **Different steps need different behaviour** — a cheap fast model for small talk, a stronger model for the step that does the real work. * **The ending matters** — always transfer to a human on request, always close the call politely. ## Building blocks [#building-blocks] | Node type | What it does | Terminal | | ---------------- | --------------------------------------------------------------- | -------- | | **Conversation** | Talks with the caller under its own instructions | No | | **Tool** | Calls your webhook in the background and advances automatically | No | | **Transfer** | Announces and executes a transfer to a phone number | Yes | | **End** | Speaks a goodbye line and hangs up | Yes | **Edges** connect nodes. Each edge carries a `description` — a plain-language condition such as *"The caller asks about available appointment slots"* — that tells the assistant when to take it, and an optional `message` spoken during the hand-over. Transitions are exclusive: one transition at a time, and the conversation context carries over so the caller never repeats themselves. **Global nodes** are conversation nodes reachable from anywhere without drawing edges. Mark a node as global (for example the reception) and describe when a caller should land there — the platform wires the transition into every other conversation step automatically. **Global prompt.** Shared context — company name, tone of voice, language — goes in the workflow's global prompt, which is prepended to every conversation node. Node instructions stay short and specific. ## Deterministic by design [#deterministic-by-design] The engine executes the things that must happen in code, not in the prompt: * **Tool nodes** call your webhook themselves and always advance along their single outgoing edge — even when the call fails, the error lands in the conversation context so the next step can respond to it. No dead air, no forgotten lookups. * **Transfer nodes** speak their announcement once and then dial. The transfer is started by the platform the moment the node becomes active. * **End nodes** finish the goodbye line cleanly before hanging up. The model decides *when* to move between steps; the platform guarantees *what happens* inside them. ## The workflow and your assistant [#the-workflow-and-your-assistant] A workflow does not replace your assistant — it drives it. When a call comes in on a number with a workflow attached: | Comes from the workflow | Stays with the assistant | | ------------------------------------------------------------- | -------------------------------------- | | System prompt per step | Default voice and speech models | | Greeting (entry node's first line) | Silence handling, max duration | | Tools per step | Voicemail detection, recording consent | | Optional model, voice, TTS and transcriber overrides per step | Webhooks, notifications, GDPR mode | | | Post-call analysis and reports | One call, one transcript, one end-of-call report — exactly as without a workflow. This also means workflows work with every assistant configuration mode, including assistants resolved dynamically through the [assistant-request webhook](/docs/webhooks/assistant-request). **Workflows drive inbound phone calls.** Outbound calls placed from the same number and web calls run the assistant's normal behaviour — attaching a workflow never changes those. **Per-step models, voices and transcribers.** Every conversation step can override parts of the assistant, and every override is validated fail-closed at call start — a misconfiguration rejects before pickup, never mid-call: * **Model** (`llm_config`): a different text model per step — a cheap fast model for small talk, a stronger one for the step that does the real work. Pipeline assistants only. * **Voice** (`voice`): a different voice per step, within the assistant's provider — on realtime assistants the speech-to-speech voice, on pipeline assistants the text-to-speech voice. A receptionist and a specialist can genuinely sound like different people. * **Text-to-speech** (`tts_config`) and **speech recognition** (`stt_config`): full per-step provider overrides including parameters — for example domain key terms or another language on one step. Pipeline assistants only; a realtime model speaks and listens itself. ## Creating a workflow [#creating-a-workflow] Build visually in the dashboard under **Workflows** — the canvas supports drag-and-drop nodes, auto-layout, keyboard shortcuts and undo/redo — or create one via the API: ```bash curl -X POST https://api.hmsovereign.com/api/v1/workflows \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Dental practice reception", "definition": { "version": 1, "entry_node": "reception", "nodes": [ { "id": "reception", "type": "conversation", "instructions": "You are the receptionist. Handle general questions.", "first_line": "Good afternoon, Riverside Dental. How can I help you?" }, { "id": "goodbye", "type": "end", "first_line": "Thanks for calling!" } ], "edges": [ { "from": "reception", "to": "goodbye", "description": "The caller indicates the conversation is finished." } ] } }' ``` Definitions are validated on write: an invalid graph — an unreachable node, a missing transition description, a transfer without a valid number — is rejected with a list of specific errors. A workflow that saves is a workflow that runs. The full field reference lives in [Workflow definitions](/docs/features/workflow-definitions). ## Attaching it to a number [#attaching-it-to-a-number] A workflow answers calls once it is attached to a phone number — in the number's settings in the dashboard, or via the API: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "WORKFLOW_UUID" }' ``` Good to know: * **Pause without detaching.** Set the workflow's `is_active` to `false` and attached numbers fall back to their assistant's normal behaviour immediately. * **Deletion is protected.** A workflow that is still attached to numbers cannot be deleted (the API returns `409`), so a live number never changes behaviour as a side effect. * **Transfers need a trunk.** If the workflow contains a transfer node, the number needs a SIP trunk for transfers, same as the [transfer tool](/docs/features/call-transfers). ## API reference [#api-reference] * [List workflows](/docs/api/workflows/listWorkflows) * [Create a workflow](/docs/api/workflows/createWorkflow) * [Get a workflow](/docs/api/workflows/getWorkflow) * [Update a workflow](/docs/api/workflows/updateWorkflow) * [Delete a workflow](/docs/api/workflows/deleteWorkflow) * [Update a phone number](/docs/api/numbers/updateNumber) (attach or detach with `workflow_id`) # Assistants Source: https://doc.voicedock.ai/docs/core-concepts/assistants Assistants are AI-powered voice entities that handle phone conversations. Each assistant has its own personality, knowledge, and capabilities defined through configuration. > **Note**: The `/api/v1/agents` endpoint is deprecated but still works as an alias for `/api/v1/assistants`. Similarly, `agent_id` works as an alias for `assistant_id` in API requests. ## Assistant Components [#assistant-components] An assistant consists of three main AI components: ### Speech-to-Text (STT) [#speech-to-text-stt] Converts the caller's speech to text. Supported providers: `deepgram`, `elevenlabs` (Scribe), `gladia`, and `mistral` (Voxtral). All run on HMS Sovereign's platform keys by default — no API key setup required. ```json { "stt_config": { "provider": "deepgram", "model": "nova-3-general", "language": "en", "keyterms": ["yes", "no", "okay"] } } ``` | Field | Description | | ---------- | ------------------------------------------------------------ | | `provider` | STT provider (`deepgram`, `elevenlabs`, `gladia`, `mistral`) | | `model` | Model name (e.g., `nova-3-general`) | | `language` | Language code (e.g., `nl`, `en`) | | `keyterms` | Optional array of words to boost recognition accuracy | ### Language Model (LLM) [#language-model-llm] The brain of your assistant. Processes the conversation and generates responses. ```json { "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.7, "messages": [ { "role": "system", "content": "You are a friendly customer service agent..." } ], "tools": [] } } ``` | Field | Description | | ------------- | ----------------------------------------------------------------------------------------- | | `provider` | LLM provider (`openai`, `mistral`, `xai`, `xai_realtime`) | | `model` | Model name (e.g., `gpt-4o-mini`, `claude-3-sonnet`) | | `temperature` | Response randomness (0-2, default 0.7) | | `messages` | System prompts defining assistant behavior | | `tools` | Custom tools the assistant can use (see [Custom Tools](/docs/configuration/custom-tools)) | > **xAI Text Models:** The `xai` provider gives you access to Grok text models (e.g. `grok-4-1-fast-non-reasoning`) and uses the same API key as `xai_realtime`. Unlike `xai_realtime`, it operates as a standard text LLM and requires separate STT and TTS configuration. ### Text-to-Speech (TTS) [#text-to-speech-tts] Converts the assistant's text responses to speech. ```json { "tts_config": { "provider": "elevenlabs", "model": "eleven_flash_v2_5", "voice_id": "21m00Tcm4TlvDq8ikWAM", "language": "nl", "stability": 0.5, "similarity_boost": 0.75, "use_speaker_boost": true } } ``` **Available Providers:** All run on HMS Sovereign's platform keys by default — no API key setup required. | Provider | Description | Access | | ------------ | -------------------------------------------------------- | ----------------------- | | `elevenlabs` | High-quality voice cloning and natural speech | Platform keys (default) | | `inworld` | EU-based TTS with natural Dutch voices (Katrien, Erik) | Platform keys (default) | | `google` | Google Gemini native TTS with style instructions support | Platform keys (default) | **ElevenLabs Models:** | Model | Description | | ------------------------ | ------------------------------------------ | | `eleven_flash_v2_5` | Fastest, low-latency (default) | | `eleven_v3` | Expressive dialogue with audio tag support | | `eleven_multilingual_v2` | Best quality, multilingual | | `eleven_turbo_v2_5` | Balanced speed and quality | | `eleven_monolingual_v1` | English only | > **Eleven v3 Audio Tags:** When using `eleven_v3`, the LLM can insert audio tags in square brackets to control the emotional delivery of speech. For example, `[sad]`, `[laughing]`, `[whispering]`, `[cheerfully]`, or `[cautiously]`. Structure your system prompt to instruct the LLM to insert these tags before or within sentences. Example: `[cheerfully] Welcome back! How can I help you today?` **Common Fields:** | Field | Description | | ---------- | -------------------------------- | | `provider` | TTS provider | | `voice_id` | Voice identifier (snake\_case) | | `language` | Language code (e.g., `nl`, `en`) | | `model` | Model name (provider-specific) | **ElevenLabs-specific:** | Field | Description | | ------------------- | ------------------------------------------------ | | `stability` | Voice consistency (0-1, default 0.5) | | `similarity_boost` | Voice similarity to original (0-1, default 0.75) | | `use_speaker_boost` | Enhance speaker clarity (boolean) | | `speed` | Speech speed multiplier | | `style` | Speaking style intensity (0-1) | ## Assistant Properties [#assistant-properties] | Property | Type | Description | | ----------------------------- | ------- | ---------------------------------------- | | `id` | UUID | Unique identifier (auto-generated) | | `name` | string | Display name for the assistant | | `business_name` | string | Business the assistant represents | | `notification_email` | email | Email for call notifications | | `first_message` | string | Initial greeting when call starts | | `is_active` | boolean | Whether assistant can handle calls | | `voicemail_detection` | boolean | Automatically detect voicemail | | `voicemail_message` | string | Message to leave on voicemail | | `max_duration_seconds` | number | Maximum call duration (null = unlimited) | | `autonomous_silence_handling` | boolean | Auto-detect silence and prompt caller | | `webhook_url` | URL | Endpoint for webhook events | | `webhook_secret` | string | Secret for signature verification | | `webhook_events` | array | Events to send to webhook | | `analysis_plan` | object | Post-call analysis configuration | ## Creating an Assistant [#creating-an-assistant] Only the `name` is required. All other fields have sensible defaults. **Minimal assistant:** ```bash curl -X POST https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "My Assistant"}' ``` **Full assistant:** ```bash curl -X POST https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Support Assistant", "business_name": "Acme Corp", "notification_email": "support@acme.com", "first_message": "Hello, welcome to Acme. How can I help you?", "is_active": true, "voicemail_detection": true, "voicemail_message": "You've reached Acme Corp. We'll call you back shortly.", "max_duration_seconds": 600, "autonomous_silence_handling": true, "webhook_url": "https://api.example.com/webhooks/hms-sovereign", "webhook_secret": "your-secret-here", "webhook_events": ["end-of-call-report", "tool-calls"], "stt_config": { "provider": "deepgram", "model": "nova-3-general", "language": "en", "keyterms": ["yes", "no", "okay"] }, "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.7, "messages": [ { "role": "system", "content": "You are a friendly customer service agent for Acme Corp." } ] }, "tts_config": { "provider": "elevenlabs", "model": "eleven_flash_v2_5", "voice_id": "21m00Tcm4TlvDq8ikWAM", "language": "en", "stability": 0.5, "similarity_boost": 0.75, "use_speaker_boost": true } }' ``` See [Create a New Assistant](/docs/api/assistants/createAssistant) for complete API documentation. ## Updating an Assistant [#updating-an-assistant] Use PATCH to update specific fields. Only provided fields are updated. ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_message": "Good afternoon, you're speaking with Acme support.", "is_active": false }' ``` See [Update an Assistant](/docs/api/assistants/updateAssistant) for details. ## Deleting an Assistant [#deleting-an-assistant] When you delete an assistant, any phone numbers assigned to it will have their `assistant_id` set to `null`. ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` See [Delete an Assistant](/docs/api/assistants/deleteAssistant) for details. ## Related [#related] * [List All Assistants](/docs/api/assistants/listAssistants) * [Get an Assistant](/docs/api/assistants/getAssistant) * [Phone Numbers](/docs/core-concepts/phone-numbers) - Assign assistants to phone numbers * [Webhooks](/docs/webhooks/overview) - Configure event notifications # Calls Source: https://doc.voicedock.ai/docs/core-concepts/calls Calls represent phone conversations handled by your AI assistants. HMS Sovereign supports both inbound calls (someone calls your number) and outbound calls (your assistant calls someone). ## Call Lifecycle [#call-lifecycle] ### Inbound Calls [#inbound-calls] 1. **Ringing** - Call comes in to your registered number 2. **Assistant Request** - If configured, webhook is called to customize the assistant 3. **In Progress** - Call is answered and conversation begins 4. **Ended** - Call ends (customer hung up, assistant ended, or transfer) 5. **Report** - End-of-call webhook sent with summary and analysis ### Outbound Calls [#outbound-calls] 1. **Dialing** - Assistant initiates call to destination 2. **In Progress** - Destination answers, conversation begins 3. **Ended** - Call ends 4. **Report** - End-of-call webhook sent ## Call Properties [#call-properties] | Property | Type | Description | | ------------------ | -------- | ---------------------------------------------- | | `id` | string | Unique call identifier | | `caller_phone` | string | Caller's phone number (E.164) | | `agent_phone` | string | Assistant's phone number (E.164) | | `status` | string | `connecting`, `in-progress`, `ended`, `failed` | | `error` | string | Error message if call failed | | `started_at` | datetime | When call was answered | | `ended_at` | datetime | When call ended | | `duration_seconds` | integer | Call duration | | `summary` | string | AI-generated call summary | | `assistant_name` | string | Name of assistant that handled call | | `business_name` | string | Business name of assistant | ## Listing Calls [#listing-calls] ```bash curl "https://api.hmsovereign.com/api/v1/calls?limit=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "calls": [ { "id": "call-_+31612345678_abc123", "caller_phone": "+31612345678", "agent_phone": "+31850835037", "status": "ended", "duration_seconds": 84, "summary": "Customer asked about opening hours and wanted to schedule an appointment.", "agent_name": "Customer Support", "business_name": "Acme Corp", "started_at": "2025-12-13T10:00:00.000Z", "ended_at": "2025-12-13T10:01:24.000Z", "created_at": "2025-12-13T09:59:55.000Z" } ], "pagination": { "total": 150, "limit": 50, "offset": 0 } } ``` ### Filtering Calls [#filtering-calls] Filter by status: ```bash curl "https://api.hmsovereign.com/api/v1/calls?status=in-progress" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Filter by date range: ```bash curl "https://api.hmsovereign.com/api/v1/calls?start_date=2025-12-01T00:00:00Z&end_date=2025-12-13T23:59:59Z" \ -H "Authorization: Bearer YOUR_API_KEY" ``` See [List Calls](/docs/api/calls/listCalls) for all parameters. ## Initiating Outbound Calls [#initiating-outbound-calls] Outbound calls require a number connected over your own [SIP trunk](/docs/configuration/sip-trunks). [Managed numbers](/docs/core-concepts/phone-numbers#managed-numbers-vs-your-own-numbers) are inbound-only and will return a `403` on outbound. Make your assistant call someone using an existing assistant configuration: ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/outbound \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+31612345678", "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "assistant_override": { "first_message": "Good afternoon, I'm calling from Acme to confirm your appointment.", "llm_config": { "messages": [ { "role": "system", "content": "Customer appointment on 2025-12-15 at 14:00. This is a reminder call." } ] } } }' ``` Or create a transient assistant with full configuration: ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/outbound \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+31612345678", "number_id": "e0f48fcd-21f4-4f20-a29b-46336aead8f9", "assistant": { "name": "Appointment Reminder Assistant", "first_message": "Good afternoon, this is a reminder for your appointment.", "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "messages": [ { "role": "system", "content": "You are a friendly appointment reminder assistant." } ] }, "tts_config": { "provider": "elevenlabs", "model": "eleven_turbo_v2_5", "voice_id": "FpLGR2n1CcG1v7SHJFsa", "language": "nl" }, "stt_config": { "provider": "deepgram", "model": "nova-2", "language": "nl" } } }' ``` **Response:** ```json { "success": true, "call_id": "call-outbound-abc123", "status": "dialing" } ``` ### Outbound Call Parameters [#outbound-call-parameters] | Parameter | Required | Description | | -------------------- | ----------- | --------------------------------------------------------------- | | `destination` | Yes | Phone number to call (E.164 format) | | `assistant_id` | Conditional | Reference to existing assistant (use with `assistant_override`) | | `assistant` | Conditional | Full transient assistant configuration | | `assistant_override` | No | Override specific fields when using `assistant_id` | | `number_id` | Conditional | Phone number to use (required if using `assistant`) | > **Note**: The `agent_id`, `agent`, and `agent_override` parameters are deprecated but still work as aliases. ### Use Cases for Outbound Calls [#use-cases-for-outbound-calls] * Appointment reminders * Customer follow-ups * Survey calls * Delivery notifications * Payment reminders See [Initiate Outbound Call](/docs/api/calls/createOutboundCall) for details. ## Real-time Call Control [#real-time-call-control] Send commands to active calls. The call must have status `in-progress`. ### Inject Context [#inject-context] Add information to the assistant's context without speaking: ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "inject-context", "content": "Customer verified: John Smith, account #12345", "trigger_response": true }' ``` ### Make Assistant Speak [#make-assistant-speak] ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "say", "content": "One moment please, I'm transferring you.", "end_after": false }' ``` ### End Call [#end-call] ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "end-call", "message": "Thank you for calling. Goodbye!" }' ``` ### Transfer Call [#transfer-call] ```bash curl -X POST https://api.hmsovereign.com/api/v1/calls/CALL_ID/control \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "transfer", "destination": "+31201234567", "message": "I'm transferring you to our sales team." }' ``` See [Send Control Command to Active Call](/docs/api/call-control/controlCall) for details. ## Related [#related] * [Assistants](/docs/core-concepts/assistants) - Configure assistants that handle calls * [Call Status Webhook](/docs/webhooks/status-update) - Receive real-time status updates * [End of Call Report](/docs/webhooks/end-of-call-report) - Get post-call summaries # Phone Numbers Source: https://doc.voicedock.ai/docs/core-concepts/phone-numbers Phone numbers are the entry point for your AI assistants. When a call comes in to a registered number, the assigned assistant handles the conversation. ## How It Works [#how-it-works] 1. You configure a phone number with your SIP provider to forward calls to HMS Sovereign 2. You register the number in HMS Sovereign via the API 3. You assign an assistant to the number 4. Incoming calls are automatically handled by the assigned assistant ## Managed numbers vs your own numbers [#managed-numbers-vs-your-own-numbers] There are two ways to get a number onto the platform, meant for two different stages. **Managed numbers** are claimed from our pool in the dashboard onboarding, with no SIP setup. They are meant for your **pilot phase** — the fastest way to get an assistant answering a real call. They come with two limits: * **One managed number per account.** * **Inbound calls only.** Outbound calls from a managed number are rejected (see [Calls](/docs/core-concepts/calls)); connect your own SIP trunk to place outbound calls. **Your own numbers (bring-your-own SIP)** are numbers you provision with your own carrier and connect over a [SIP trunk](/docs/configuration/sip-trunks). This is the recommended path for production: the most stable connections, lowest cost, telephony you keep under your own control, and full inbound *and* outbound. Registering a number via the API (below) is how you bring your own number onto the platform. ## Phone Number Properties [#phone-number-properties] | Property | Type | Description | | ------------------- | ------ | --------------------------------------------------- | | `id` | UUID | Unique identifier | | `phone_number` | string | Phone number in E.164 format (e.g., `+31850835037`) | | `name` | string | Display name for the number | | `assistant_id` | UUID | Assigned assistant (nullable) | | `transfer_trunk_id` | UUID | SIP trunk for call transfers (nullable) | ## Registering a Phone Number [#registering-a-phone-number] ```bash curl -X POST https://api.hmsovereign.com/api/v1/numbers \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+31850835037", "name": "Main Support Line", "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88" }' ``` **Response:** ```json { "number": { "id": "abc12345-1234-5678-9012-abcdef123456", "phone_number": "+31850835037", "name": "Main Support Line", "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "transfer_trunk_id": null, "created_at": "2025-12-13T10:00:00.000Z", "updated_at": "2025-12-13T10:00:00.000Z" } } ``` See [Register a Phone Number](/docs/api/numbers/createNumber) for details. ## Updating Assistant Assignment [#updating-assistant-assignment] Change which assistant handles calls for a number: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "assistant_id": "new-assistant-uuid-here" }' ``` To unassign an assistant (calls will not be handled): ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "assistant_id": null }' ``` See [Update a Phone Number](/docs/api/numbers/updateNumber) for details. ## Attaching a Workflow [#attaching-a-workflow] A number can run a [workflow](/docs/features/workflows) instead of a single assistant prompt: the workflow drives the conversation, while the assigned assistant keeps supplying the voice, models and call settings. ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "workflow-uuid-here" }' ``` Pass `"workflow_id": null` to detach and return to normal assistant behaviour. ## Configuring Call Transfers [#configuring-call-transfers] To enable call transfers from this number, assign a SIP trunk: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "transfer_trunk_id": "trunk-uuid-here" }' ``` See [SIP Trunks](/docs/configuration/sip-trunks) for trunk configuration. ## Number-Level Webhook [#number-level-webhook] A phone number can carry its own webhook, configured in the dashboard under **Phone numbers → (a number) → Webhooks**: a URL, an optional signing secret, and the events to send (status updates and end-of-call reports). A number's webhook is used when its assigned assistant has no webhook of its own, and takes precedence over the account-wide webhook. This is especially useful for numbers that resolve their assistant dynamically at call time — the [Assistant Request](/docs/webhooks/assistant-request) is sent to the number's webhook so your server can return the right assistant. See [Webhooks overview](/docs/webhooks/overview) for the full precedence order. ## Listing All Numbers [#listing-all-numbers] ```bash curl https://api.hmsovereign.com/api/v1/numbers \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "numbers": [ { "id": "abc12345-...", "phone_number": "+31850835037", "name": "Main Support Line", "assistant_id": "17a0cb75-...", "transfer_trunk_id": null }, { "id": "def67890-...", "phone_number": "+31201234567", "name": "Sales Line", "assistant_id": "28b1dc86-...", "transfer_trunk_id": "trunk-123-..." } ] } ``` See [List All Phone Numbers](/docs/api/numbers/listNumbers) for details. ## Deleting a Phone Number [#deleting-a-phone-number] ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/numbers/NUMBER_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` See [Delete a Phone Number](/docs/api/numbers/deleteNumber) for details. ## Related [#related] * [Assistants](/docs/core-concepts/assistants) - Create assistants to assign to numbers * [SIP Trunks](/docs/configuration/sip-trunks) - Configure trunks for call transfers * [Calls](/docs/core-concepts/calls) - View call history for your numbers # Webhooks Source: https://doc.voicedock.ai/docs/core-concepts/webhooks Webhooks allow you to receive real-time notifications about call events. Configure a webhook URL on your assistant to receive HTTP POST requests when events occur. ## Webhook Events [#webhook-events] | Event | When Triggered | Response Used | | -------------------- | --------------------------------- | ----------------------------- | | `assistant-request` | Before call is answered | Yes - can configure assistant | | `tool-calls` | Assistant needs to execute a tool | Yes - return tool result | | `status-update` | Call status changes | No | | `end-of-call-report` | After call ends | No | ## Configuring Webhooks [#configuring-webhooks] Set the webhook URL and events on your assistant: ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{ "webhook_url": "https://api.example.com/webhooks/hms-sovereign", "webhook_secret": "your-secret-for-verification", "webhook_events": ["assistant-request", "tool-calls", "status-update", "end-of-call-report"] }' ``` ## Configuration levels [#configuration-levels] Webhooks can be defined at more than one level. For each event, the event is delivered to **exactly one** endpoint, chosen by precedence: 1. **Custom tool** (tool-calls only) — a webhook on the specific tool 2. **Assistant** — the assistant handling the call 3. **Phone number** — the number the call came in on 4. **Account** — an organization-wide fallback The first level that has a webhook for that event wins. A phone number's webhook is used when its assigned assistant has none, and takes precedence over the account webhook. Assistant webhooks are set via the API or the dashboard; phone-number webhooks are set in the dashboard under **Phone numbers → (a number) → Webhooks**. ## Webhook Headers [#webhook-headers] All webhook requests include these headers: | Header | Description | | --------------------- | -------------------------------------------- | | `Content-Type` | `application/json` | | `User-Assistant` | `HMS-Sovereign/1.0` | | `X-Webhook-Event` | Event type (e.g., `tool-calls`) | | `X-Webhook-Timestamp` | Unix timestamp of the request | | `X-Webhook-Signature` | HMAC-SHA256 signature (if secret configured) | ## Signature Verification [#signature-verification] If you configure a `webhook_secret`, verify the signature to ensure requests come from HMS Sovereign: **Python:** ```python import hmac import hashlib def verify_signature(payload: str, secret: str, timestamp: str, signature: str) -> bool: sig_hex = signature.removeprefix("sha256=") message = f"{timestamp}.{payload}" expected = hmac.new( secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(sig_hex, expected) # In your webhook handler: payload = request.body.decode() timestamp = request.headers.get("X-Webhook-Timestamp") signature = request.headers.get("X-Webhook-Signature") if not verify_signature(payload, "your-secret", timestamp, signature): return Response("Invalid signature", status=401) ``` **Node.js:** ```javascript const crypto = require('crypto'); function verifySignature(payload, secret, timestamp, signature) { const sigHex = signature.replace(/^sha256=/, ''); const message = `${timestamp}.${payload}`; const expected = crypto .createHmac('sha256', secret) .update(message) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(sigHex), Buffer.from(expected) ); } ``` ## Event Details [#event-details] ### Assistant Request [#assistant-request] Called **before** an inbound call is answered. Allows you to: * Customize the assistant configuration per call * Personalize the greeting based on caller * Reject calls (e.g., outside business hours) **Timeout:** 5 seconds. If your endpoint doesn't respond in time, the call proceeds with default configuration. **Response format:** ```json { "assistant_id": "uuid", // Reference mode: use saved assistant "assistant": { ... }, // Transient mode: full assistant config "assistant_override": { ... } // Hybrid mode: merge with assistant_id } ``` See [Dynamic Assistant Configuration Webhook](/docs/webhooks/assistant-request) for payload details. ### Tool Calls [#tool-calls] Called when the assistant needs to execute a custom tool during the conversation. **Timeout:** 10 seconds for synchronous tools. Response formats supported: * Object format: `{"results": [{"tool_call_id": "...", "result": {...}}]}` * Simple format: `{"result": {...}}` * Direct format: `{...}` (your data directly) * Error format: `{"error": "message"}` See [Tool/Function Call Webhook](/docs/webhooks/tool-calls) for payload details. ### Status Update [#status-update] Called when the call status changes: * `in-progress` - Call connected * `ended` - Call ended normally * `ended-with-error` - Call ended due to error Response is ignored. See [Call Status Update Webhook](/docs/webhooks/status-update) for payload details. ### End of Call Report [#end-of-call-report] Called after the call ends with: * Call duration * AI-generated summary * Structured analysis (if `analysis_plan` configured) Response is ignored. See [End of Call Report Webhook](/docs/webhooks/end-of-call-report) for payload details. ## Common Payload Fields [#common-payload-fields] All webhooks include this structure: ```json { "message": { "type": "status-update", "timestamp": "2025-12-13T12:00:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "in-progress" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" }, "assistant": { "id": "assistant-uuid", "name": "Customer Support Assistant", "llm_config": { ... }, "tts_config": { ... }, "stt_config": { ... } } } } ``` ## Best Practices [#best-practices] 1. **Respond quickly** - Return 200 OK as fast as possible, especially for status updates 2. **Process asynchronously** - Queue heavy processing for later 3. **Verify signatures** - Always verify webhook signatures in production 4. **Handle retries** - Implement idempotency for duplicate deliveries 5. **Log everything** - Keep webhook logs for debugging ## Related [#related] * [Custom Tools Guide](/docs/configuration/custom-tools) - Building tools for your assistant * [Call Analysis Guide](/docs/features/call-analysis) - Structured post-call data * [Webhook Security](/docs/webhooks/security) - Securing your webhooks # Authentication Source: https://doc.voicedock.ai/docs/get-started/authentication All API requests require authentication using a Bearer token in the `Authorization` header. ## Getting Your API Key [#getting-your-api-key] 1. Log in to the HMS Sovereign dashboard 2. Navigate to **Developer > Your API Key** 3. Copy your API key ## Making Authenticated Requests [#making-authenticated-requests] Include your API key in the `Authorization` header with every request: ```bash curl https://api.hmsovereign.com/api/v1/account \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Request Headers [#request-headers] | Header | Value | Required | | --------------- | --------------------- | ----------------------- | | `Authorization` | `Bearer YOUR_API_KEY` | Yes | | `Content-Type` | `application/json` | For POST/PATCH requests | ## Example: Get Account Information [#example-get-account-information] ```bash curl https://api.hmsovereign.com/api/v1/account \ -H "Authorization: Bearer fl_live_abc123xyz..." ``` **Response:** ```json { "id": "user-uuid-here", "email": "you@example.com", "company_name": "Your Company", "minutes_balance": 500, "monthly_usage_minutes": 120, "active_numbers": 3, "active_agents": 2, "active_calls": 0 } ``` See [Get Account Information](/docs/api/organization/getOrganization) for full details. ## Rate Limits [#rate-limits] The API enforces the following rate limits: | Limit Type | Rate | | --------------------- | -------------------------------------- | | General API requests | 100 requests per minute per API key | | Call control commands | 10 commands per minute per active call | When you exceed a rate limit, the API returns a `429 Too Many Requests` response. ## Error Responses [#error-responses] ### 401 Unauthorized [#401-unauthorized] Returned when the API key is missing, invalid, or expired. ```json { "error": "Unauthorized", "message": "Invalid or missing API key" } ``` ### 403 Forbidden [#403-forbidden] Returned when your API key doesn't have permission for the requested resource. ```json { "error": "Forbidden", "message": "You don't have access to this resource" } ``` ## Security Best Practices [#security-best-practices] 1. **Never expose your API key in client-side code** - Always make API calls from your server 2. **Rotate keys periodically** - Generate new keys and revoke old ones regularly 3. **Use environment variables** - Store keys in environment variables, not in code 4. **Monitor usage** - Check your account dashboard for unexpected API activity # Quickstart Source: https://doc.voicedock.ai/docs/get-started/quickstart This guide walks you through creating your first AI voice assistant and making it available on a phone number. ## Prerequisites [#prerequisites] * An HMS Sovereign account with an API key * A phone number configured to forward calls to HMS Sovereign (contact support for SIP configuration) ## Step 1: Get Your API Key [#step-1-get-your-api-key] Find your API key in the dashboard under **Developer > Your API Key**. You'll use this for all API requests. ## Step 2: Create an Assistant [#step-2-create-an-assistant] Create a new AI assistant with a simple POST request: ```bash curl -X POST https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer Support Assistant", "business_name": "Acme Corp", "first_message": "Hello, welcome to Acme. How can I help you?", "llm_config": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.7, "messages": [ { "role": "system", "content": "You are a friendly customer service agent for Acme Corp. Answer questions about opening hours, products and services. Be helpful and professional." } ] }, "stt_config": { "provider": "deepgram", "model": "nova-3-general", "language": "en" }, "tts_config": { "provider": "openai", "voice_id": "alloy", "language": "en" } }' ``` **Response:** ```json { "assistant": { "id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "name": "Customer Support Assistant", "business_name": "Acme Corp", "first_message": "Hello, welcome to Acme. How can I help you?", "is_active": true, "stt_config": { ... }, "llm_config": { ... }, "tts_config": { ... }, "created_at": "2025-12-13T10:00:00.000Z", "updated_at": "2025-12-13T10:00:00.000Z" } } ``` Save the `id` - you'll need it to assign a phone number. **Note:** The `stt_config`, `llm_config`, and `tts_config` providers above (Deepgram, OpenAI, and any other supported provider) run on HMS Sovereign's platform keys by default — you don't need to set up any API keys to create a working assistant. [Bring Your Own Key](../integrations/byok-setup.md) is optional, for when you'd rather use your own provider accounts. ## Step 3: Register a Phone Number [#step-3-register-a-phone-number] Register your phone number and assign the assistant: ```bash curl -X POST https://api.hmsovereign.com/api/v1/numbers \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+31850835037", "name": "Main Support Line", "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88" }' ``` **Response:** ```json { "number": { "id": "abc12345-1234-5678-9012-abcdef123456", "phone_number": "+31850835037", "name": "Main Support Line", "assistant_id": "17a0cb75-fa09-4bdd-9a44-92a70d829c88", "created_at": "2025-12-13T10:05:00.000Z" } } ``` ## Step 4: Test Your Assistant [#step-4-test-your-assistant] Call the phone number you registered. Your assistant will answer and greet the caller with the configured first message. ## Step 5: View Call History [#step-5-view-call-history] After the call, view it in your call history: ```bash curl https://api.hmsovereign.com/api/v1/calls \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "calls": [ { "id": "call-_+31612345678_abc123", "caller_phone": "+31612345678", "assistant_phone": "+31850835037", "status": "ended", "duration_seconds": 45, "summary": "Customer asked about opening hours.", "assistant_name": "Customer Support Assistant", "created_at": "2025-12-13T10:10:00.000Z" } ], "pagination": { "total": 1, "limit": 100, "offset": 0 } } ``` ## Next Steps [#next-steps] * [Configure webhooks](/docs/webhooks/overview) to receive real-time call events * [Add custom tools](/docs/configuration/custom-tools) to let your assistant look up information * [Set up BYOK](/docs/integrations/byok-setup) to use your own API keys * [Enable call analysis](/docs/features/call-analysis) for structured post-call data # Data Processing Agreement Source: https://doc.voicedock.ai/docs/guides/data-processing-agreement VoiceDock offers a Data Processing Agreement under Article 28 GDPR, at no cost, to every customer processing personal data through the platform. It is no longer published on this page. ## Where to get it [#where-to-get-it] Request access through the [VoiceDock Trust Center](https://trust.voicedock.ai/#contact). Tell us who you are and why you need it, and we release the current version once the request is verified. Alongside the DPA, the Trust Center holds the security overview, the sub-processor list with each provider's processing location and transfer basis, the retention policy, the incident-response and business-continuity statements, and self-serve answers to the questions a vendor security assessment usually asks. ## Why it is not simply downloadable [#why-it-is-not-simply-downloadable] A Data Processing Agreement is a contract with a named counterparty, not a public notice. Publishing it as an open download meant we had no record of who held which version, which is exactly what we need when a version changes and the people relying on it have to be told. The documents the GDPR requires us to make openly available, the [Privacy Policy](https://trust.voicedock.ai/legal/privacy-policy) and the [Cookie Policy](https://trust.voicedock.ai/legal/cookie-policy), stay public and unrestricted. Those are information duties. The DPA is an agreement. ## Practical [#practical] * **Cost:** none. It is included in your subscription. * **Scope:** it covers all processing of customer data under the agreement, **in every configuration of the service**. Which sub-processors are engaged, where data is processed and how long it is retained follow from your configuration; the agreement sets out those consequences rather than limiting its own scope to one setup. * **Signature:** electronic, signed on our side by the director of Flireo B.V. * **Earlier versions:** available on request for audit or record-keeping. * **Redlines:** small changes such as notice addresses can usually be handled within the standard process. Substantive changes to liability, audit or sub-processor terms are handled as a custom agreement. * **Translations:** Dutch and other European languages on request. ## Corrected [#corrected] An earlier version of this page described the DPA as applying **only** to one specific configuration (Vertex AI in the EU together with GDPR Mode), and stated that reconfiguring an assistant caused its protections to lapse automatically for subsequent processing. That was the old scoping and it no longer holds. The current agreement applies in every configuration. If you signed on the basis of the earlier text, the current version is broader rather than narrower, but ask us and we will confirm your position in writing. ## Contact [#contact] **Flireo B.V.** · Leeuwenbrug 89a, 7411 TH Deventer, The Netherlands · KvK 92548806 | Subject | Contact | | --------------------------------- | ------------------------------------------------------------- | | Requesting the DPA | [VoiceDock Trust Center](https://trust.voicedock.ai/#contact) | | Privacy and data-subject requests | [privacy@flireo.com](mailto:privacy@flireo.com) | | Contractual notices | [legal@flireo.com](mailto:legal@flireo.com) | | Security incidents | [security@flireo.com](mailto:security@flireo.com) | ## Related [#related] * [EU-resident configuration](/docs/platform/eu-data-sovereignty) — how to configure an assistant that processes only in the EU * [Privacy & Compliance](/docs/features/privacy-compliance) — GDPR Mode and recording consent # BYOK Setup Source: https://doc.voicedock.ai/docs/integrations/byok-setup Bring Your Own Key (BYOK) is an optional advanced feature that lets you use your own API keys for AI providers, giving you full control over costs and the option to bill providers directly. It is **not** required: by default every assistant runs on HMS Sovereign's platform keys, and every supported provider works out of the box with no API keys of your own. Set up BYOK only when you specifically want to bring your own provider account. **Note:** Specifying a provider on an assistant does not require a BYOK key. If you select a provider without configuring a key for it, the assistant simply uses HMS Sovereign's platform keys for that provider. BYOK only overrides which key is used — it never unlocks a provider. ## Supported Providers [#supported-providers] | Provider | Category | Use For | | ---------- | -------- | -------------- | | Deepgram | STT | Speech-to-Text | | OpenAI | LLM | Language Model | | ElevenLabs | TTS | Text-to-Speech | | Resend | Email | Notifications | ## Step 1: Get Your API Keys [#step-1-get-your-api-keys] ### Deepgram (STT) [#deepgram-stt] 1. Sign up at [https://console.deepgram.com](https://console.deepgram.com) 2. Create an API key with "Member" role 3. Copy the key (it won't be shown again) ### OpenAI (LLM) [#openai-llm] 1. Go to [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) 2. Click "Create new secret key" 3. Copy the key (starts with `sk-`) ### ElevenLabs (TTS) [#elevenlabs-tts] 1. Go to [https://elevenlabs.io/app/settings/api-keys](https://elevenlabs.io/app/settings/api-keys) 2. Click "Create API Key" 3. Copy the key ### Resend (Email) [#resend-email] 1. Go to [https://resend.com/api-keys](https://resend.com/api-keys) 2. Click "Create API Key" 3. Copy the key (starts with `re_`) ## Step 2: Add BYOK Configuration [#step-2-add-byok-configuration] Add your API key via the API: ```bash curl -X POST https://api.hmsovereign.com/api/v1/byok \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "openai", "api_key": "sk-..." }' ``` Repeat for each provider you want to configure. ## Step 3: Verify Configuration [#step-3-verify-configuration] Check which providers are configured: ```bash curl https://api.hmsovereign.com/api/v1/byok \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "byok_keys": { "deepgram_secret_id": "vault-id-1", "openai_secret_id": "vault-id-2", "elevenlabs_secret_id": null, "resend_secret_id": null } } ``` A non-null value means the provider is configured. ## Step 4: Configure Your Assistant [#step-4-configure-your-assistant] Create or update your assistant to use the provider: ```bash curl -X POST https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My Assistant", "stt_config": { "provider": "deepgram", "model": "nova-3-general", "language": "nl" }, "llm_config": { "provider": "openai", "model": "gpt-4o", "temperature": 0.7, "messages": [...] }, "tts_config": { "provider": "elevenlabs", "voice_id": "your-voice-id" } }' ``` ## Available Models [#available-models] Check available models for each provider: ```bash curl https://api.hmsovereign.com/api/v1/byok/config \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Important Notes for ElevenLabs [#important-notes-for-elevenlabs] When using ElevenLabs with BYOK: 1. **Voice Library Only** - You only have access to voices in your ElevenLabs Voice Library 2. **No Public Voices** - You don't automatically get access to all public ElevenLabs voices 3. **Add Voices First** - Before using a voice, add it to your library at [https://elevenlabs.io/app/voice-library](https://elevenlabs.io/app/voice-library) 4. **Voice IDs** - Use the voice ID from your ElevenLabs dashboard ## Removing BYOK Configuration [#removing-byok-configuration] Removing a provider's BYOK key makes that provider fall back to HMS Sovereign's platform keys — the default that every assistant starts on. Your assistant keeps working with no further changes: ```bash curl -X DELETE https://api.hmsovereign.com/api/v1/byok \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "openai" }' ``` ## Security [#security] * API keys are stored securely and encrypted at rest * Keys are never exposed in API responses * Only encrypted references are stored in your account * You can rotate keys at any time by adding a new key for the same provider See [BYOK API Reference](/docs/api/byok/getBYOKConfigs) for complete details. # MCP Server Source: https://doc.voicedock.ai/docs/integrations/mcp-server VoiceDock exposes a hosted [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server at `mcp.hmsovereign.com`. This lets AI assistants — including Claude, Claude Code, Cursor, and any other MCP-compatible tool — interact with your VoiceDock account directly, without leaving your environment. Once connected, your AI assistant can list assistants, initiate calls, check usage, manage campaigns, and perform any other operation available in the API — all through natural language. ## Endpoint [#endpoint] ``` https://mcp.hmsovereign.com/mcp ``` The server dynamically loads the VoiceDock OpenAPI spec and exposes every API endpoint as an MCP tool. It stays in sync automatically — no configuration changes are required when the API is updated. ## Authentication [#authentication] The MCP server is an **OAuth 2.1 resource server**. You connect with just the URL above — no key to copy. Your client discovers the sign-in flow automatically, opens a browser where you **log in with your VoiceDock account** and approve access, and then works against your organisation. Behind the scenes we map your account to your organisation's credentials; you never handle an API key. > **Tip:** Prefer a static token (CI, scripts, servers)? A raw organisation API key still works as a `Bearer` token — see [Legacy: API key](#legacy-api-key) below. ## Setup [#setup] ### Claude Code / Cursor / Claude Desktop [#claude-code--cursor--claude-desktop] Add the endpoint and let the client run the sign-in flow: ```json { "mcpServers": { "voicedock": { "type": "http", "url": "https://mcp.hmsovereign.com/mcp" } } } ``` The first time you connect, a browser opens: sign in with your VoiceDock account and click **Allow** on the consent screen. That's it — the tools appear in your assistant. * **Claude Desktop**: Settings → Connectors → *Add custom connector* → paste the URL. * **Claude Code**: `claude mcp add --transport http voicedock https://mcp.hmsovereign.com/mcp` (or add the JSON above). * **Cursor**: MCP settings → add the JSON above. ### Other MCP clients [#other-mcp-clients] Any client that supports the [MCP Streamable HTTP transport](https://modelcontextprotocol.io/docs/concepts/transports) and OAuth can connect with the URL alone: * **URL**: `https://mcp.hmsovereign.com/mcp` * **Transport**: Streamable HTTP * **Auth**: OAuth 2.1 (the server advertises its authorization server via protected-resource metadata; clients register dynamically and prompt you to sign in) ### Legacy: API key [#legacy-api-key] If your client can't run an OAuth flow, or you're wiring this into a server or CI job, pass an organisation API key as a bearer token instead: ```json { "mcpServers": { "voicedock": { "type": "http", "url": "https://mcp.hmsovereign.com/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Find your API key in the dashboard under **Developer → Your API Key**. Both methods resolve to the same organisation. ## Available Tools [#available-tools] The MCP server exposes all VoiceDock API endpoints as tools — one tool per endpoint. Examples: | Tool | Description | | -------------------- | -------------------------------------------------- | | `listAssistants` | List all voice assistants in your organization | | `createAssistant` | Create a new voice assistant | | `getAssistant` | Retrieve a specific assistant by ID | | `updateAssistant` | Update assistant configuration | | `createOutboundCall` | Initiate an outbound call | | `listCalls` | List calls with optional filters | | `getCall` | Get call details including transcript and analysis | | `createCampaign` | Create an outbound call campaign | | `listVoices` | Browse available TTS voices | | `getUsage` | Retrieve usage and billing data | The full list of tools mirrors the [API reference](/docs/api/assistants/listAssistants). ## Example Usage [#example-usage] Once connected, you can ask your AI assistant to perform tasks in plain language: > *"Create a new assistant called 'Support Bot' with a friendly greeting and GPT-4o as the language model."* > *"List all calls from this week and summarize the outcomes."* > *"Start an outbound call to +31612345678 using assistant ID xyz."* > *"Show me my usage for the last 30 days."* ## Security [#security] * **OAuth 2.1 with your own account** — no long-lived key to copy, share, or leak. Access is tied to your VoiceDock login, shown on an explicit consent screen, and revocable from your client at any time. * The MCP server is **stateless** — no session data is retained between requests. * All tool calls are **scoped to your organisation**, derived from your signed-in identity (or, for the legacy path, your API key). * Traffic is TLS-only and the token is never logged by the MCP server. * **Only approve connections you started yourself.** The consent screen names the app requesting access — if you don't recognise it, click *Deny*. ## Troubleshooting [#troubleshooting] **The browser sign-in doesn't open** Make sure your client supports remote MCP servers with OAuth (recent Claude Desktop, Claude Code, and Cursor do). If it can't, use the [legacy API-key](#legacy-api-key) method instead. **Tools not appearing after connecting** Reconnect the server so the client re-fetches the tool list. For the legacy path, verify your API key is valid: ```bash curl https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Server unavailable** Check [status.voicedock.ai](https://status.voicedock.ai) for current platform status. *** > **Note**: The MCP server is read/write — connected AI assistants can create, update, and delete resources on your behalf. Approve only trusted apps on the consent screen, and keep any legacy API key to trusted environments. # Provider Pricing Source: https://doc.voicedock.ai/docs/integrations/provider-pricing Last updated: March 2026 All prices in USD. These are the API costs charged by providers - not HMS Sovereign pricing to customers. *** ## Speech-to-Text (STT) [#speech-to-text-stt] ### Deepgram [#deepgram] | Model | Price per Minute | | --------------------- | ---------------- | | Nova 3 (Multilingual) | $0.0092 | | Nova 3 (Monolingual) | $0.0077 | | Nova 2 | $0.0058 | | Nova 1 | $0.0058 | | Enhanced | $0.0165 | | Base | $0.0145 | **Note:** Prices are Pay-As-You-Go tier. Growth tier is \~17% cheaper. ### Gladia [#gladia] | Model | Price per Hour | | ------------------- | -------------- | | Solaria (Async) | $0.61 | | Solaria (Real-time) | $0.75 | **Converted to per minute:** \~$0.0102/min (async), \~$0.0125/min (real-time) *** ## Language Models (LLM) [#language-models-llm] ### OpenAI [#openai] Prices per 1M tokens. | Model | Input | Output | | ------------------- | ------ | ------- | | **GPT-5 Mini** | $0.25 | $2.00 | | **GPT-4.1** | $2.00 | $8.00 | | **GPT-4.1 Mini** | $0.40 | $1.60 | | **GPT-4.1 Nano** | $0.10 | $0.40 | | **GPT-4o** | $2.50 | $10.00 | | GPT-4o (2024-05-13) | $5.00 | $15.00 | | **GPT-4o Mini** | $0.15 | $0.60 | | GPT-4 Turbo | $10.00 | $30.00 | | GPT-4 | $30.00 | $60.00 | | GPT-4 32K | $60.00 | $120.00 | | GPT-3.5 Turbo | $0.50 | $1.50 | | GPT-3.5 Turbo 16K | $3.00 | $4.00 | **Recommended for voice assistants:** GPT-5 Mini (best value), GPT-4o Mini (fastest), GPT-4.1 Mini (balanced) ### Mistral [#mistral] Prices per 1M tokens. | Model | Input | Output | | ------------------ | ----- | ------ | | **Mistral Large** | $0.50 | $1.50 | | **Mistral Medium** | $0.40 | $2.00 | | **Mistral Small** | $0.10 | $0.30 | | **Ministral 8B** | $0.15 | $0.15 | | Ministral 3B | $0.10 | $0.10 | | **Codestral** | $0.30 | $0.90 | | Mixtral 8x7B | $0.70 | $0.70 | | Mixtral 8x22B | $2.00 | $6.00 | **Recommended for voice assistants:** Mistral Small (fast + cheap), Mistral Medium (balanced) ### xAI (Grok) [#xai-grok] Prices per 1M tokens. | Model | Input | Output | | -------------------- | ----- | ------ | | **Grok 4.1 Fast** | $0.20 | $0.50 | | **Grok 4 Fast** | $0.20 | $0.50 | | **Grok Code Fast 1** | $0.20 | $1.50 | | Grok 4 (0709) | $3.00 | $15.00 | | **Grok 3 Mini** | $0.30 | $0.50 | | **Grok 3** | $3.00 | $15.00 | **Realtime API (Speech-to-Speech):** | Model | Price | | -------------------- | -------------------- | | **Grok Realtime v1** | $0.05/min ($3.00/hr) | **Recommended:** Grok 4.1 Fast (best value), Grok Realtime (for S2S) *** ## Text-to-Speech (TTS) [#text-to-speech-tts] ### ElevenLabs [#elevenlabs] Prices per 1,000 characters. Based on Creator tier ($22/mo). | Model | Price per 1K chars | | ------------------- | ------------------ | | **Flash v2.5** | $0.11 | | **Turbo v2.5** | $0.11 | | **Eleven v3** | $0.22 | | **Multilingual v2** | $0.22 | | Monolingual v1 | $0.22 | **Tier pricing breakdown:** | Tier | Flash/Turbo per 1K | Multilingual per 1K | | ----------------- | ------------------ | ------------------- | | Free | N/A | $0.17 | | Starter ($5) | $0.08 | $0.17 | | Creator ($22) | $0.11 | $0.22 | | Pro ($99) | $0.10 | $0.20 | | Scale ($330) | $0.08 | $0.17 | | Business ($1,320) | $0.06 | $0.12 | **Recommended:** Flash v2.5 (fastest, cheapest), Multilingual v2 (best quality) ### Inworld [#inworld] Prices per 1,000,000 characters (On-demand tier). | Model | Price per 1M chars | Per 1K chars | | ---------------- | ------------------ | ------------ | | **TTS 1.5 Mini** | $5.00 | $0.005 | | **TTS 1.5 Max** | $10.00 | $0.01 | | TTS 1 | $5.00 | $0.005 | | TTS 1 Max | $10.00 | $0.01 | **Note:** Inworld is \~20x cheaper than ElevenLabs! At 650 chars/min: * Inworld 1.5-Mini: $0.00325/min * Inworld 1.5-Max: $0.0065/min * ElevenLabs Flash: $0.0715/min *** ## How These Costs Map to Your Bill [#how-these-costs-map-to-your-bill] The tables above are the raw provider rates. How they reach your HMS Sovereign bill depends on the mode you run an assistant in. By default an assistant runs on HMS Sovereign's platform keys — every provider listed here works out of the box, with no API keys of your own required. | Mode | What you pay | | --------------------------------------- | ---------------------------------------------------------------- | | Platform keys (default) | Model usage at cost + €0,07/min orchestration | | Vertex AI Live (Google Gemini realtime) | €0,25/min all-in | | Bring Your Own Key (optional) | Your provider's usage (billed by them) + €0,07/min orchestration | | Local models (Whisper, Piper) | Free + €0,07/min orchestration | *1 credit = €0,07 (one minute of orchestration).* On platform keys, the model usage from the tables above is passed through at cost and the only HMS Sovereign markup is the €0,07/min orchestration fee. Choosing a lighter STT/LLM/TTS combination lowers the at-cost model portion of your bill; the orchestration fee stays the same. With Bring Your Own Key (optional), your provider bills you directly for that usage instead and HMS Sovereign charges only the €0,07/min orchestration fee. Local models (Whisper, Piper) carry no model-usage charge, leaving just the €0,07/min orchestration fee. # xAI Grok Integration Source: https://doc.voicedock.ai/docs/integrations/xai-grok-integration xAI's Grok Realtime API provides speech-to-speech conversation with \<700ms latency. Unlike traditional voice AI (STT → LLM → TTS), Grok processes audio directly in a single model. ## Setup [#setup] xAI Grok is available as a provider choice on HMS Sovereign's platform keys by default — you can select it when configuring an assistant without adding any API key first. ### 1. (Optional) Add your own xAI API Key [#1-optional-add-your-own-xai-api-key] This step is optional, only needed to run Grok on your own xAI account. To use platform keys, skip straight to step 2. To bring your own key, navigate to **Integrations** → **API Keys** tab and add it: ```bash curl -X POST https://api.hmsovereign.com/api/v1/byok -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{ "provider": "xai", "api_key": "xai-..." }' ``` ### 2. Configure Assistant [#2-configure-assistant] When creating or editing an assistant with xAI configured: * **Provider**: Select "xAI Realtime" * **Model**: see the table below * **Voice**: `ara` (or any of the 26 available voices) **Note**: When using xAI Realtime, separate STT/TTS providers are ignored. ### Models [#models] | Model | Price | Notes | | --------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `grok-voice-think-fast-2.0` | $0.08 / min | xAI's newest speech-to-speech model. Backwards compatible with the 1.0 settings, so voices and turn-taking carry over unchanged. | | `grok-voice-think-fast-1.0` | $0.05 / min | Default for new assistants. | | `grok-voice-fast-1.0` | $0.05 / min | | | `grok-voice-latest` | follows the target | Rolling alias. Points at 1.0 until 5 August 2026 and at 2.0 after, which changes the per-minute price with it. Pin an explicit model if you would rather decide that moment yourself. | `grok-realtime-v1` is xAI's retired name. Assistants that still store it keep working, but do not pick it for anything new. ## Pricing [#pricing] * **Platform keys (default):** xAI model usage at cost + €0,07/min orchestration. No API key setup needed. * **Bring Your Own Key (optional):** €0,07/min orchestration, with xAI API usage billed directly to your own xAI account and no markup on it. Per-minute rates are xAI's list prices and cover speech recognition, the model and speech synthesis together, so no separate STT or TTS cost applies. ## Differences from Traditional Mode [#differences-from-traditional-mode] | Feature | Traditional (STT+LLM+TTS) | xAI Realtime | | ------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- | | Latency | \~1-2 seconds | \<700ms | | Providers | 3 separate | Single (xAI) | | Voice Quality | Depends on TTS provider | Native to model | | Custom Tools | Supported via `llm_config.tools` | Check xAI docs for support | | API Keys | None required (platform keys); optionally BYOK 3 providers (STT, LLM, TTS) | None required (platform keys); optionally BYOK 1 provider (xAI) | ## Limitations [#limitations] * Custom system prompts may work differently than OpenAI * Tool calling support depends on xAI API capabilities * Voice selection limited to xAI's available voices ## API Reference [#api-reference] See [BYOK API Reference](/docs/api/byok/getBYOKConfigs) for managing xAI API keys. # VoiceDock, Flireo & HMS Sovereign Source: https://doc.voicedock.ai/docs/platform/about You will come across three names across this documentation, our domains, and your contracts: **Flireo B.V.**, **VoiceDock**, and **HMS Sovereign**. They are not separate companies or competing products. They are one company, one product, and the technology that powers it. ## The short version [#the-short-version] | Name | What it is | Where you meet it | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Flireo B.V.** | The company. A Dutch private limited company (Besloten Vennootschap) that builds and operates everything below. | Your contracting party: invoices, the Data Processing Agreement, and legal terms are with Flireo B.V. | | **VoiceDock** | The product. The voice AI platform you build on: the API, the dashboard, the MCP server, and the documentation you are reading now. | The brand and the platform: `voicedock.ai`, your account, your assistants. | | **HMS Sovereign** | The technology. The orchestration engine that powers VoiceDock and runs your calls in real time. | Under the hood: some infrastructure and legacy domains still carry the `hmsovereign` name, for example `api.hmsovereign.com`. | ## In one sentence [#in-one-sentence] **Flireo B.V. builds and operates VoiceDock, which runs on the HMS Sovereign orchestrator.** ## Why there are three names [#why-there-are-three-names] VoiceDock began as HMS Sovereign: the in-house orchestration engine we built to run voice AI on our own European infrastructure. As we opened that engine up for others to build on, the product, the brand and the developer experience became **VoiceDock**. **HMS Sovereign** kept its name as the orchestration technology underneath. So you may still see `hmsovereign` in a few technical places (the API and dashboard domains, some infrastructure hostnames) while the product, the brand and the account you work with are **VoiceDock**. Both point to the same platform, operated by the same company: **Flireo B.V.** ## What this means for you [#what-this-means-for-you] * **Contracts and compliance** are with **Flireo B.V.**, the legal entity. How Flireo B.V. handles your data is documented in one place, the [VoiceDock Trust Center](https://trust.voicedock.ai): the privacy policy, the sub-processor list, the retention policy and the Data Processing Agreement. * **The product you build on** is **VoiceDock**: your API keys, assistants, dashboard and this documentation. * **The technology** running your calls is the **HMS Sovereign** orchestrator, hosted on EU infrastructure (see [EU-resident configuration](/docs/platform/eu-data-sovereignty)). # Billing & Credits Source: https://doc.voicedock.ai/docs/platform/billing HMS Sovereign uses a credits-based billing system. You purchase credits upfront and they are consumed as you make and receive calls. ## Pricing [#pricing] By default an assistant runs on HMS Sovereign's platform keys — every supported provider works out of the box, with no API keys required. What you pay depends on which mode you run an assistant in: | Mode | What you pay | | --------------------------------------- | ---------------------------------------------------------------- | | Platform keys (default) | Model usage at cost + €0,07/min orchestration | | Vertex AI Live (Google Gemini realtime) | €0,25/min all-in (model) + telephony | | Bring Your Own Key (optional) | Your provider's usage (billed by them) + €0,07/min orchestration | | Local models (Whisper, Piper) | Free + €0,07/min orchestration | *1 credit = €0,07 (one minute of orchestration).* On **managed phone numbers** (numbers you claim from the platform), telephony is metered as a provider cost at carrier rates, alongside model usage. Numbers you bring on your **own SIP trunk are exempt** — your carrier bills you directly, exactly like BYOK for models. See [Telephony costs](#telephony-costs) below. ## Telephony Costs [#telephony-costs] Calls on managed numbers use the platform's carrier account, so the phone legs are billed at cost — itemized per call in the cost breakdown, next to model usage: | Leg | Rate | Billing increments | | -------------------------------------------- | ---------- | ------------------ | | Inbound, national number (085) | $0.016/min | per 6 seconds | | Inbound, local number (geographic) | $0.008/min | per 6 seconds | | Outbound to a fixed line (call transfers) | $0.010/min | per whole minute | | Outbound to a mobile number (call transfers) | $0.040/min | per whole minute | Rates are USD carrier list prices, converted to credits with the same exchange-rate-plus-buffer applied to model usage. How the legs add up: * A normal call uses **one leg**: the inbound line, for the call's duration. * A [transferred call](/docs/features/call-transfers) uses **two legs** once connected: the inbound line keeps running for the whole conversation, and an outbound leg to the destination runs from the moment the transfer connects. Both appear as separate telephony lines in the call's cost breakdown. * A transfer attempt that is **never answered costs nothing** (no outbound leg is billed — relevant with [no-answer handling](/docs/features/call-transfers#no-answer-handling-monitored-transfers)). * **Own trunk?** Telephony lines still show on the call for transparency, but are marked exempt and never billed. ## Concurrency & Fair Use [#concurrency--fair-use] There is no concurrency fee and no per-line charge. You pay per minute, and simultaneous calls simply run. As a fair-use guideline, we consider up to **20 simultaneous calls per account** normal use. Nothing hard-blocks at that number: the guideline exists so a runaway dialer or a misfiring script can't degrade the platform for everyone else. If you expect to run more than 20 lines at once (a call center, an outbound campaign), tell us in advance and we'll make sure the capacity is there. Structurally higher volumes and guaranteed capacity (we hold lines free for you, even at platform peak) are available on contract. Why we can do this: sustained concurrency generates minutes, and minutes are what you already pay for. Most voice platforms charge around $10 per month for every concurrent line beyond 10. We think that double-charges the same thing. ## How Credits Work [#how-credits-work] 1. **Purchase credits** - Add credits to your account via the dashboard 2. **Use credits** - Credits are consumed based on call duration 3. **Track usage** - Monitor your balance and usage in real-time ## Credit Balance [#credit-balance] Check your current balance via the API: ```bash curl https://api.hmsovereign.com/api/v1/account \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "id": "org-uuid", "company_name": "Your Company", "minutes_balance": 500, "monthly_usage_minutes": 120 } ``` | Field | Description | | ----------------------- | ------------------------ | | `minutes_balance` | Available credit minutes | | `monthly_usage_minutes` | Minutes used this month | ## Usage Tracking [#usage-tracking] View detailed usage logs: ```bash curl "https://api.hmsovereign.com/api/v1/usage?start_date=2025-12-01&end_date=2025-12-31" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "usage": [ { "id": "usage-uuid", "duration_sec": 84, "created_at": "2025-12-13T10:00:00.000Z", "number_id": "number-uuid" } ], "summary": { "total_seconds": 7200, "total_minutes": 120, "total_cost": 8.40 } } ``` ## Insufficient Credits [#insufficient-credits] When your balance reaches zero: **Inbound calls:** An error audio message is played to the caller and the call is logged as `status: rejected`. **Outbound calls:** The API returns `402 Payment Required`: ```json { "error": { "code": "insufficient_credits", "message": "Insufficient credits to make outbound call. Please add credits in your billing dashboard." } } ``` ## Adding Credits [#adding-credits] Credits can be added through the dashboard: 1. Navigate to **Settings > Billing** 2. Click **Add Credits** 3. Select the amount 4. Complete payment Payment methods accepted: * Credit card (Visa, Mastercard, American Express) ## Invoices [#invoices] All credit purchases generate an invoice with: * Invoice number * Purchase amount * VAT (if applicable, based on your location) * PDF download Access invoices in **Settings > Billing > Invoices**. ## Tax Information [#tax-information] For EU businesses: * If you provide a valid VAT ID, reverse charge applies (0% VAT) * Without VAT ID, destination country VAT rate applies To add billing information: 1. Go to **Settings > Billing** 2. Add your company name, address, and VAT ID 3. Information is synced to Stripe for invoice generation ## Low Balance Notifications [#low-balance-notifications] Set up a webhook to monitor your balance and receive alerts: ```javascript // Check balance after each call via end-of-call-report webhook app.post('/webhook', async (req, res) => { const account = await fetch('https://api.hmsovereign.com/api/v1/account', { headers: { 'Authorization': 'Bearer ' + API_KEY } }).then(r => r.json()); if (account.minutes_balance < 100) { await sendAlert(`Low balance warning: ${account.minutes_balance} minutes remaining`); } res.status(200).send('OK'); }); ``` ## Choosing a Pricing Mode [#choosing-a-pricing-mode] Every mode charges the same €0,07/min orchestration fee. The difference is who bills you for model usage: * **Platform keys (default).** You don't manage any provider keys. Model usage is billed at cost through HMS Sovereign on top of the €0,07/min orchestration fee. This is what a new assistant uses out of the box. * **Vertex AI Live.** A single all-in rate of €0,25/min for Google Gemini realtime, with nothing else to configure. * **Bring Your Own Key (optional).** You supply your own provider keys, so your provider bills you directly for their usage and HMS Sovereign only charges the €0,07/min orchestration fee. Use this when you want full control over provider costs or want to use your own provider plan, features, and models. BYOK is never required. * **Local models (Whisper, Piper).** Self-hosted models run free of model-usage charges; you pay only the €0,07/min orchestration fee. *** ## Related [#related] * [Usage API](/docs/api/usage/getUsage) * [Account API](/docs/api/organization/getOrganization) * [BYOK Setup](../integrations/byok-setup.md) # Dashboard Security Source: https://doc.voicedock.ai/docs/platform/dashboard-security HMS Sovereign's dashboard is built with security as a foundational requirement. This document describes the authentication model, access controls, and protective measures in place for all dashboard users. *** ## Authentication [#authentication] ### Password-Based Login [#password-based-login] All accounts are secured with email/password authentication. Passwords must be at least **8 characters** and are never stored in plaintext. Authentication is handled via an open-source identity server (GoTrue) running on HMS Sovereign's own EU-hosted infrastructure. ### PKCE Flow [#pkce-flow] Authentication tokens are exchanged using the **Proof Key for Code Exchange (PKCE)** flow. This prevents authorization code interception attacks by binding the token exchange to the originating client. All email confirmation links, password reset links, and OAuth callbacks use PKCE. ### GitHub OAuth [#github-oauth] Users can sign in with GitHub as an alternative to email/password. The OAuth code is exchanged server-side before any session is established. ### Session Management [#session-management] Sessions are managed via **secure, HTTP-only cookies**. Sessions are automatically refreshed on activity and validated on every request by the server-side middleware. There are no long-lived tokens stored in browser localStorage. *** ## Two-Factor Authentication (2FA) [#two-factor-authentication-2fa] HMS Sovereign supports **TOTP-based two-factor authentication** (Time-based One-Time Password), compatible with any standard authenticator app (Google Authenticator, Authy, 1Password, etc.). ### How It Works [#how-it-works] 1. Enable 2FA in **Settings → Security** 2. Scan the QR code with your authenticator app 3. Enter the 6-digit code to confirm enrollment 4. On future logins, you will be prompted for your 6-digit code after your password ### Assurance Levels [#assurance-levels] The authentication system tracks assurance levels per session: | Level | Meaning | | ----- | ------------------------------------------------- | | AAL1 | Authenticated with password only | | AAL2 | Authenticated with password + second factor (2FA) | Protected routes require at minimum AAL1. Users with 2FA enrolled are required to complete AAL2 before accessing the dashboard. ### Disabling 2FA [#disabling-2fa] 2FA can be disabled from **Settings → Security**. This requires re-authenticating with your current TOTP code. *** ## API Key Security [#api-key-security] ### Key Format [#key-format] API keys use the format `fl_<64 hex characters>`, generated using cryptographically secure random bytes. ### Storage [#storage] API keys are **never stored in plaintext** in the database. Each key is encrypted and stored in a dedicated secrets vault (using envelope encryption). The database only stores a reference ID to the vault entry, not the key itself. ### Usage [#usage] Keys are passed as Bearer tokens in the `Authorization` header: ``` Authorization: Bearer fl_... ``` Keys are validated on every API request before any data is returned or action is taken. ### Rotation [#rotation] API keys can be regenerated at any time from **Developer → Your API Key**. Regenerating a key immediately invalidates the previous key. Old vault entries are deleted on rotation. *** ## Access Control [#access-control] ### Role-Based Access [#role-based-access] The dashboard distinguishes between two roles: | Role | Access | | --------- | ------------------------------------------------------------------------------------------------ | | **User** | Full access to their own organization's data | | **Admin** | Additional access to platform-wide admin features (account management, credits, support tickets) | ### Organization Isolation [#organization-isolation] All data is scoped to your organization. Every database query filters by `org_id`, ensuring organizations cannot access each other's data. This is enforced both at the application layer and at the database level via Row-Level Security (RLS) policies. ### Protected Routes [#protected-routes] The following routes require an active authenticated session: * `/dashboard` — Overview * `/agents` — Voice assistants * `/numbers` — Phone numbers * `/calls` — Call history * `/usage` — Usage & billing * `/settings` — Account settings * `/integrations` — BYOK & provider setup * `/campaigns` — Outbound campaigns * `/admin` — Admin panel (admin role required) Unauthenticated requests to protected routes are redirected to the login page. *** ## Email Verification & Password Reset [#email-verification--password-reset] ### Email Verification [#email-verification] New accounts receive a confirmation email before access is granted. The confirmation link uses a time-limited, signed token exchanged via the PKCE flow. Unverified accounts cannot sign in. ### Password Reset [#password-reset] 1. Request a reset link on the **Forgot Password** page 2. A signed recovery link is sent to your email address 3. Clicking the link initiates a PKCE token exchange 4. You are directed to set a new password (minimum 8 characters) Recovery links expire after a short time window and can only be used once. *** ## Rate Limiting [#rate-limiting] API endpoints are rate-limited to prevent abuse: | Endpoint | Limit | | ----------------- | ----------------------------------- | | API requests | 100 requests/minute per API key | | Outbound calls | 20 requests/minute per organization | | Domain management | 5 requests/hour per organization | Rate limit status is returned in response headers: * `X-RateLimit-Remaining` — Requests remaining in the current window * `X-RateLimit-Reset` — Unix timestamp when the limit resets *** ## Encryption [#encryption] * **In transit**: all connections use TLS. HTTP is redirected to HTTPS. * **At rest**: API keys and BYOK secrets are held as managed, encrypted secrets, never in plaintext in the database, and are scoped to the organisation that owns them. *** ## Infrastructure, monitoring and certifications [#infrastructure-monitoring-and-certifications] Where the dashboard and platform data are hosted, which sub-processors are involved, what our error monitoring does and does not capture, and which certifications apply to which layer are all stated in the [VoiceDock Trust Center](https://trust.voicedock.ai). Keeping a second description of it here meant the two drifted apart. **Corrected 2026-07-30.** An earlier version of this page stated that Sentry session replays were enabled with `maskAllText` and `blockAllMedia`. That was wrong in a way worth naming: the Sentry **browser** SDK is not loaded at all, so there is no session recording to mask. Error monitoring is server-side only, pinned to Sentry's EU region, with personal data disabled at the SDK level. *** ## Reporting a vulnerability [#reporting-a-vulnerability] Email **[security@flireo.com](mailto:security@flireo.com)**. Our published policy, including the response times we commit to and what is in scope, is the [Vulnerability Disclosure Policy](https://trust.voicedock.ai/legal/vulnerability-disclosure). # EU-resident configuration Source: https://doc.voicedock.ai/docs/platform/eu-data-sovereignty This page is a configuration guide: how to set up an assistant whose speech-to-text, language model and text-to-speech all process inside the EU. It deliberately no longer states where each provider processes, how long they retain data, or on what legal basis a transfer takes place. Those statements changed whenever a provider changed their terms, and keeping them accurate across three separate places did not work: some of them were wrong for months. They now live in one place, and this page links to it. **Where the compliance statements live** Our data-residency position, the sub-processor list with each provider's processing location and transfer basis, the retention periods and the Data Processing Agreement are all in the [VoiceDock Trust Center](https://trust.voicedock.ai). The public documents are readable there directly. The confidential ones, including the DPA, the security overview and the full sub-processor annex, are released on request once we have verified who is asking. If you need to know what your own setup currently does, ask us: we can tell you per assistant which component processes where. > **VoiceDock** is the platform, **Flireo B.V.** is the company that operates it, and **HMS Sovereign** is the orchestration technology that runs your calls. See [VoiceDock, Flireo & HMS Sovereign](/docs/platform/about) for how the three relate. ## What is fixed and what you choose [#what-is-fixed-and-what-you-choose] The platform layer is EU-resident in every configuration: real-time voice processing runs on dedicated servers in Germany, and platform data is stored in an EU cloud region. The AI providers are your choice. Speech-to-text, the language model and text-to-speech each call an external provider unless you use our on-premise models, and providers differ in where they process. The setup below keeps all three inside the EU. ## Data flow [#data-flow] ```mermaid flowchart LR A[Caller] --> B[VoiceDock
Nuremberg, DE] B --> C[On-premise STT
Nuremberg, DE] C --> D[Mistral LLM
EU] D --> E[On-premise TTS
Nuremberg, DE] E --> B B --> A style A fill:#1e40af,color:#fff style B fill:#f97316,color:#fff style C fill:#f97316,color:#fff style D fill:#8b5cf6,color:#fff style E fill:#f97316,color:#fff ``` This is the flow for the setup below. Configure a different provider for any of the three steps and call content goes wherever that provider processes it. ## Setup [#setup] ### Step 1: Enable on-premise speech-to-text [#step-1-enable-on-premise-speech-to-text] On-premise STT runs on our own hardware in Germany and requires `has_local_models_access` on your organisation. Contact **[support@flireo.com](mailto:support@flireo.com)** to have it enabled; there is no API key to configure. If you prefer an external speech-to-text provider, add its key the same way as Step 2. ### Step 2: Add a Mistral API key [#step-2-add-a-mistral-api-key] ```bash curl -X POST https://api.hmsovereign.com/api/v1/byok \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "mistral", "api_key": "YOUR_MISTRAL_KEY" }' ``` Get a key at [mistral.ai](https://mistral.ai). Supplying your own key changes whose account is billed, and puts that provider's retention and training settings under your control. It does not change who the processor is: we still route the audio and make the request from our own infrastructure, so the provider remains our sub-processor. ### Step 3: Configure your assistant [#step-3-configure-your-assistant] ```bash curl -X POST https://api.hmsovereign.com/api/v1/assistants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "EU-Resident Assistant", "stt_config": { "provider": "local", "model": "medium", "language": "en" }, "llm_config": { "provider": "mistral", "model": "mistral-medium-latest", "temperature": 0.7, "messages": [{"role": "system", "content": "Your system prompt here"}] }, "tts_config": { "provider": "local" } }' ``` ### Step 4: Verify [#step-4-verify] Place a call to your assistant's number. With this configuration, speech-to-text and text-to-speech run on our own EU infrastructure and the language model runs at Mistral in the EU. ## Storing less in the first place [#storing-less-in-the-first-place] Residency is one lever. Not storing the content at all is the other, and it is often the stronger answer. **GDPR Mode** stops transcripts, recordings, summaries and analysis from being stored. **Recording consent** requires the caller to press 1 before any recording or AI processing begins. Both are documented with their configuration fields in [Privacy & Compliance](/docs/features/privacy-compliance). ## On-premise deployment [#on-premise-deployment] For organisations that cannot accept any external dependency, the full stack can run on hardware you own. Scoping that is a conversation rather than a configuration: contact **[support@flireo.com](mailto:support@flireo.com)**. ## Related [#related] * [Privacy & Compliance](/docs/features/privacy-compliance) — GDPR Mode and recording consent, with configuration fields * [BYOK Setup](/docs/integrations/byok-setup) — bringing your own provider keys * [VoiceDock Trust Center](https://trust.voicedock.ai) — residency, sub-processors, retention and the DPA # Voice Selection Psychology Source: https://doc.voicedock.ai/docs/platform/voice-selection-psychology When deploying AI phone assistants, voice selection is often underestimated. Many assume that the most human-like, natural-sounding voice is always the best choice. However, real-world deployments reveal a more nuanced picture. ## The Uncanny Valley of Voice AI [#the-uncanny-valley-of-voice-ai] When callers know they're speaking with an AI (as required by transparency regulations), but the voice sounds indistinguishable from a human, a psychological tension emerges. This creates what we call the **"Uncanny Valley of Conversation"**: * Callers feel uncomfortable being direct with something that sounds human * They hesitate to give short, efficient answers like "Yes" or "No" * Social norms around politeness and small talk feel awkward to ignore * The mismatch between knowing it's AI and hearing a human voice causes cognitive friction ## The Case for Robotic Voices [#the-case-for-robotic-voices] Our experience implementing hundreds of AI assistants in production environments has revealed a counterintuitive finding: **slightly robotic voices often outperform natural voices** in specific use cases. ### Why Robotic Voices Work [#why-robotic-voices-work] 1. **Permission to be Direct** When a voice clearly signals "I am a machine," callers feel comfortable responding efficiently. They don't feel rude saying "No" without explanation or answering questions without pleasantries. 2. **Reduced Social Pressure** Human-sounding voices trigger social scripts. Callers feel obligated to be polite, make small talk, or soften rejections. A robotic voice removes this pressure. 3. **Clearer Expectations** Callers immediately understand the interaction paradigm. They know to speak clearly, answer directly, and that the system won't be offended by brevity. 4. **Faster Interactions** Without the social overhead of human-like conversation, calls complete more quickly. Both parties get to the point faster. 5. **Higher Completion Rates** In many deployments, we've observed that callers are more likely to complete interactions with robotic voices because the interaction feels less awkward. ## When to Use Each Voice Type [#when-to-use-each-voice-type] ### Use Robotic/Local Voices For: [#use-roboticlocal-voices-for] | Use Case | Why It Works | | ----------------------------- | -------------------------------------------------------- | | **Appointment Confirmations** | Callers just need to say "Yes" or reschedule | | **Payment Reminders** | Direct, transactional interactions | | **Survey Collection** | Clear questions, simple answers | | **Status Updates** | Information delivery, minimal back-and-forth | | **Verification Calls** | "Please confirm your date of birth" | | **Queue Callbacks** | "Your table is ready" or "A representative is available" | | **Inbound Support Triage** | Routing calls to the right department | ### Use Human-Like Voices For: [#use-human-like-voices-for] | Use Case | Why It Works | | ------------------------- | ---------------------------------------------------- | | **Sales Calls** | Building rapport and trust matters | | **Complex Support** | Empathy and patience feel important | | **Sensitive Topics** | Healthcare, financial hardship, complaints | | **Relationship Building** | When the call itself is part of the brand experience | | **High-Value Customers** | Premium experience expectations | | **Persuasion Required** | Negotiations, upsells, retention | ## The Technical Trade-Off [#the-technical-trade-off] Beyond psychology, there's a practical consideration: | Aspect | Robotic/Local Voice | Human-Like Voice | | ----------------- | ------------------- | ------------------------- | | **Latency** | Very low (\~50ms) | Higher (\~200-500ms) | | **Cost** | Minimal | Per-character billing | | **Reliability** | No API dependencies | External service required | | **Languages** | Limited selection | Wide variety | | **Customization** | Fixed voices | Voice cloning available | ## Our Recommendation [#our-recommendation] **Start with robotic voices for transactional use cases.** You may be surprised by the results. Many teams default to expensive, natural-sounding voices assuming they're better, only to find that: * Callers respond faster * Completion rates are higher * Costs are significantly lower * Latency is reduced Then A/B test with natural voices for use cases where relationship-building matters. ## The Optimal Configuration [#the-optimal-configuration] For most AI phone assistants, we recommend: | Component | Recommendation | Why | | ------------------------ | ------------------------------------ | -------------------------------------------------- | | **STT (Speech-to-Text)** | Premium provider (Deepgram, etc.) | Accurate understanding is critical | | **LLM (Language Model)** | Powerful model (GPT-4, Claude, etc.) | Reasoning, instruction-following, function calling | | **TTS (Text-to-Speech)** | Consider local/robotic | Often improves user experience | The intelligence should be in understanding and reasoning. The voice is just the delivery mechanism, and a clearly artificial voice can actually improve the interaction. ## Summary [#summary] Don't assume human-like is always better. Match your voice selection to your use case: * **Transactional, efficient interactions** → Robotic voice * **Relationship-building, emotional interactions** → Human-like voice Test both. Measure completion rates, call duration, and user satisfaction. The results may surprise you. # Whitelabel Source: https://doc.voicedock.ai/docs/platform/whitelabel The whitelabel feature lets you build your own branded AI calling business. Create a custom portal where your clients sign up, purchase credits, and make AI calls — all under your brand. Whitelabel enables agencies and resellers to offer HMS Sovereign's AI calling technology under their own brand, with custom pricing, payment processing, and user management. ## What You Can Do [#what-you-can-do] | Feature | Description | | --------------- | ---------------------------------------- | | Custom Branding | Your logo, colors, and company name | | Your Pricing | Set your own credit packages and margins | | Your Payments | Connect your Stripe account | | Custom Domain | Use your own portal URL | | User Management | Manage clients and allocate credits | | Full Control | Enable/disable portal anytime | ## How It Works [#how-it-works] When a user signs up on your whitelabel portal, a dedicated **child organization** is automatically created in your HMS Sovereign account. This gives you full control over each client's AI assistant configuration directly from your dashboard. **The billing flow:** 1. You pay HMS Sovereign €0,07/minute orchestration. On platform keys, model usage is billed at cost on top of that, so your real per-minute HMS cost is €0,07 plus the at-cost provider charges for the STT/LLM/TTS models in use. 2. Your clients purchase credits from your portal (your price) 3. When clients make calls, minutes are deducted from your HMS account 4. You keep the margin between your price and your total HMS cost > **Factor in provider costs.** Unless your organization is on a custom flat negotiated rate, platform-key calls add at-cost provider charges on top of the €0,07/min orchestration. Base your client pricing on the full per-minute cost (orchestration + provider usage), not on €0,07 alone. Provider usage varies by model — premium TTS or larger LLMs cost more than budget models. ## Per-Client Assistant Management [#per-client-assistant-management] Each whitelabel user gets their own organization in your HMS Sovereign dashboard. This enables you to build complete, customized agentic systems for every client. ### What You Can Configure Per Client [#what-you-can-configure-per-client] | Configuration | Description | | ----------------- | -------------------------------------------------------------- | | Custom Assistants | Create unique AI assistants tailored to each client's business | | Webhooks | Configure separate endpoints for each client | | Integrations | Connect client-specific CRMs, calendars, and tools | | Knowledge Bases | Upload client-specific documents and data | | Voice Settings | Customize voice, language, and personality per client | ### Webhook Configuration [#webhook-configuration] Set up dedicated webhooks for each client organization to receive real-time events: | Event | Description | | -------------------- | ----------------------------------------------------------- | | `assistant-request` | Triggered when the assistant needs external data or actions | | `status-updates` | Call status changes (ringing, answered, ended, etc.) | | `tool-calls` | When the AI assistant invokes a configured tool | | `end-of-call-report` | Detailed summary after each call completes | This allows you to route events to client-specific backend systems, enabling deep integrations with their existing infrastructure. ### Building Custom Services [#building-custom-services] The per-client organization model opens up additional revenue streams beyond credit margins: **Integration Services** * Connect each client's CRM (Salesforce, HubSpot, Pipedrive, etc.) * Sync with their calendar systems (Google Calendar, Outlook) * Integrate with email platforms for follow-ups * Connect to their helpdesk or ticketing system **Custom Assistant Development** * Build specialized assistants for each client's industry * Create unique conversation flows and scripts * Configure client-specific tools and functions * Train assistants on client knowledge bases **Revenue Model Example** | Revenue Stream | Example | | ------------------- | ----------------------------------------------------------- | | Credit Margin | Sell credits at €0.40, cost €0.07 = €0.33 profit per minute | | Setup Fee | Charge €500-2000 for initial assistant configuration | | Monthly Service | Charge €200-500/month for managed assistant services | | Custom Integrations | Charge €1000+ for CRM/calendar/email integrations | > **Note:** The €0.07 cost in the credit-margin row is the orchestration fee only. On platform keys, at-cost provider usage is added on top, so your true per-minute cost — and therefore your real margin — is lower than this example suggests. Custom-priced organizations on a flat negotiated rate are the exception: their per-minute cost is that fixed rate. ### Managing Client Organizations [#managing-client-organizations] To manage a client's organization: 1. Navigate to the organization switcher in your HMS dashboard 2. Select the client's organization from the dropdown 3. Configure assistants, webhooks, and integrations as needed 4. Switch back to your parent organization when done > **Note:** Child organizations inherit your minute balance. All usage is billed to your parent account, and you handle billing to your clients through your whitelabel portal. ## Quick Start [#quick-start] ### Step 1: Access Whitelabel Settings [#step-1-access-whitelabel-settings] Navigate to **Whitelabel** in your HMS Sovereign dashboard sidebar. > **Note:** Only parent organizations can access whitelabel. If you signed up through another agency's portal, you won't see this option. ### Step 2: Configure Branding [#step-2-configure-branding] Set up your portal's appearance: | Field | Description | | ------------- | ----------------------------------------------- | | Company Name | Your brand name displayed throughout the portal | | Logo URL | URL to your company logo | | Primary Color | Main brand color (hex, e.g., `#3B82F6`) | | Accent Color | Secondary color for highlights | ### Step 3: Set Your Portal URL [#step-3-set-your-portal-url] Choose how clients access your portal: **Option A: Subdomain (Automatic)** ``` {your-slug}.portal.hmsovereign.com ``` **Option B: Custom Domain (Optional)** ``` portal.yourdomain.com ``` > **Tip:** Slugs must be 3-50 characters, lowercase letters, numbers, and hyphens only. ### Step 4: Connect Stripe [#step-4-connect-stripe] Set up payment processing — see the detailed Stripe setup section below. ### Step 5: Activate Portal [#step-5-activate-portal] Toggle "Portal Active" to make your portal live. All required fields must be complete. ## Stripe Configuration [#stripe-configuration] Connect your own Stripe account to accept payments from your clients. ### Define Your Credits Package [#define-your-credits-package] Decide your pricing strategy: | Field | Description | Example | | ------------------ | --------------------------------------- | --------------- | | Credits in Package | Number of credits (1 credit = 1 minute) | 100 | | Package Price | Price in cents | 4000 (= €40.00) | | Currency | EUR, USD, or GBP | EUR | ### Create Product in Stripe [#create-product-in-stripe] 1. Go to [Stripe Dashboard](https://dashboard.stripe.com) → Products → Add Product 2. Name it (e.g., "100 AI Calling Credits") 3. Set as **one-time payment** 4. Set price to match your configured amount 5. Copy the **Product ID** (`prod_...`) and **Price ID** (`price_...`) ### Enter Stripe Credentials [#enter-stripe-credentials] | Field | Description | | --------------- | ------------------------------------------------------------ | | Publishable Key | Your Stripe publishable key (`pk_live_...` or `pk_test_...`) | | Secret Key | Your Stripe secret key (stored securely) | | Product ID | From previous step (`prod_...`) | | Price ID | From previous step (`price_...`) | ### Configure Webhook [#configure-webhook] Your webhook URL will be displayed in the settings: ``` https://portal.hmsovereign.com/api/webhooks/stripe/{your-slug} ``` 1. Go to Stripe Dashboard → Webhooks → Add endpoint 2. Enter the webhook URL shown in HMS settings 3. Select the `checkout.session.completed` event 4. Copy the webhook signing secret and paste it in HMS settings > **Important:** Keep your Stripe secret key and webhook secret secure. They are stored encrypted and never exposed in API responses. ### Margin Calculator [#margin-calculator] The dashboard includes a built-in margin calculator: | Metric | Example | | ------------- | -------------------- | | Package Price | €40.00 (100 credits) | | Your Cost | €7.00 (100 × €0.07) | | Your Profit | €33.00 | | Margin | 82.5% | > **Important:** The "Your Cost" figure here counts only the €0,07/min orchestration. On platform keys you also pay at-cost provider charges for the STT/LLM/TTS models your clients use, so your actual cost per minute is higher and your real margin is lower than shown. Factor provider usage into your pricing. (Custom-priced organizations on a flat negotiated rate keep that rate as their full per-minute cost.) > **Tip:** Price your credits to balance competitiveness with profitability. Most agencies charge €0.20-0.50 per credit. ## Custom Domain Setup [#custom-domain-setup] Use your own domain for a fully branded experience. 1. In Portal URL settings, enter your custom domain (e.g., `portal.yourdomain.com`) 2. Click "Add Domain" — the system will register your domain and display DNS records 3. Add the CNAME record at your DNS provider: | Type | Name | Value | | ----- | ------ | -------------------- | | CNAME | portal | cname.vercel-dns.com | 4. Click "Refresh Status" to check verification. DNS changes can take up to 48 hours. ## User Management [#user-management] View and manage your portal users from the HMS dashboard. ### User Table [#user-table] | Column | Description | | --------------- | ---------------------- | | Email | Client's email address | | Credits Balance | Available credits | | Actions | Allocate credits | ### Allocating Credits Manually [#allocating-credits-manually] You can add credits to users without a purchase (e.g., promotional credits, refunds): 1. Locate the user in the Users table 2. Click the "Allocate" button 3. Enter the **Credits Amount** and a **Description** (e.g., "Invoice #INV-2024-001") 4. Click "Allocate" to add credits immediately > **Note:** Users appear in the table after they sign up on your portal. They're automatically linked to your agency. ## Billing Details [#billing-details] ### Credit Flow [#credit-flow] 1. **You** maintain a minutes balance in HMS Sovereign 2. **Your clients** have virtual credits in your portal 3. When a client makes a call: * Their credits are deducted in real-time * Your HMS minutes are deducted equally 4. Monitor your balance to ensure uninterrupted service ### Purchase Flow [#purchase-flow] When your client buys credits: 1. Client clicks "Buy Credits" on your portal 2. Portal creates a Stripe Checkout session 3. Client completes payment on Stripe 4. Stripe sends webhook (`checkout.session.completed`) to your portal 5. Portal adds credits to the user's account 6. Client sees confirmation ### Usage Flow [#usage-flow] When your client makes a call: 1. Client initiates AI call via HMS API 2. HMS logs the call duration 3. Minutes are deducted from your (parent) HMS account 4. Portal syncs the credit deduction 5. Client's balance is updated ## Portal Requirements [#portal-requirements] Your portal activates when all required fields are complete: | Requirement | Status | | ---------------------- | -------- | | Company Name | Required | | Portal Slug | Required | | Stripe Publishable Key | Required | | Stripe Secret Key | Required | | Stripe Product ID | Required | | Stripe Price ID | Required | | Logo URL | Optional | | Custom Domain | Optional | > **Tip:** Test your Stripe integration in test mode before going live. Use `pk_test_...` and `sk_test_...` keys. ## Troubleshooting [#troubleshooting] ### Portal won't activate [#portal-wont-activate] Ensure all required fields are filled: * Company name * Portal slug * All four Stripe fields (publishable key, secret key, product ID, price ID) Check for validation errors highlighted in red. ### Custom domain not verifying [#custom-domain-not-verifying] 1. Verify DNS records are correctly configured 2. Wait for DNS propagation (up to 48 hours) 3. Click "Refresh Status" to recheck 4. Ensure no conflicting A or AAAA records exist ### Payments not crediting users [#payments-not-crediting-users] 1. Verify webhook URL is correctly configured in Stripe 2. Check webhook signing secret matches 3. Ensure `checkout.session.completed` event is selected 4. Check Stripe webhook logs for delivery errors ### Client credits not deducting [#client-credits-not-deducting] 1. Ensure your HMS account has sufficient minutes 2. Check that the client's organization is linked to your parent org 3. Contact support if the issue persists ### Users not appearing in table [#users-not-appearing-in-table] Users only appear after signing up on your portal. They must complete registration to be linked to your agency. ## Best Practices [#best-practices] ### Pricing Strategy [#pricing-strategy] * Research competitor pricing * Consider volume discounts * Factor in support costs * Leave room for promotions * Don't price below your cost * Don't change prices too frequently ### Branding [#branding] * Use a high-quality logo (PNG or SVG) * Choose accessible color contrast * Keep branding consistent across all materials * Use a custom domain for increased trust ### User Management [#user-management-1] * Respond to support requests quickly * Document all manual credit allocations * Monitor user activity regularly * Set up alerts for low balances ### Payments [#payments] * Test in Stripe test mode before going live * Monitor webhook deliveries in Stripe dashboard * Keep Stripe keys secure — never share them * Enable Stripe email receipts for your customers ## Related [#related] * [Billing & Credits](../webhooks/security.md) - How HMS billing works * [Account API](/docs/api/organization/getOrganization) - Check your balance * [Usage API](/docs/api/usage/getUsage) - Track usage details # Error Codes Source: https://doc.voicedock.ai/docs/reference/error-codes This reference lists all error codes returned by the HMS Sovereign API with explanations and recommended actions. ## HTTP Status Codes [#http-status-codes] ### Success Codes [#success-codes] | Code | Name | Description | | ----- | ---------- | ---------------------------------------------------------- | | `200` | OK | Request succeeded | | `201` | Created | Resource created successfully | | `204` | No Content | Request succeeded, no content returned (delete operations) | ### Client Error Codes [#client-error-codes] | Code | Name | Description | | ----- | -------------------- | -------------------------------------------- | | `400` | Bad Request | Invalid request format or validation error | | `401` | Unauthorized | Missing or invalid API key | | `402` | Payment Required | Insufficient credits balance | | `403` | Forbidden | Valid API key but no access to this resource | | `404` | Not Found | Resource doesn't exist | | `409` | Conflict | Resource already exists (duplicate) | | `422` | Unprocessable Entity | Request understood but cannot be processed | | `429` | Too Many Requests | Rate limit exceeded | ### Server Error Codes [#server-error-codes] | Code | Name | Description | | ----- | --------------------- | ------------------------------ | | `500` | Internal Server Error | Unexpected server error | | `502` | Bad Gateway | Upstream service unavailable | | `503` | Service Unavailable | Server temporarily unavailable | | `504` | Gateway Timeout | Upstream service timeout | *** ## Error Response Format [#error-response-format] All errors follow this format: ```json { "error": { "code": "error_code", "message": "Human readable message", "param": "field_name", "type": "error_type" } } ``` | Field | Description | | --------- | ----------------------------------------------- | | `code` | Machine-readable error code | | `message` | Human-readable description | | `param` | The field that caused the error (if applicable) | | `type` | Error category | *** ## Error Codes by Category [#error-codes-by-category] ### Authentication Errors [#authentication-errors] | Code | Message | Solution | | ----------------- | -------------------------- | ---------------------------------------------------------------------- | | `unauthorized` | Invalid or missing API key | Check your API key is correct and included in the Authorization header | | `api_key_expired` | API key has expired | Generate a new API key in the dashboard | | `api_key_revoked` | API key has been revoked | Generate a new API key | ### Validation Errors [#validation-errors] | Code | Message | Solution | | ---------------------- | --------------------------- | -------------------------------------------------- | | `invalid_request` | Request body is invalid | Check JSON syntax and required fields | | `missing_field` | Missing required field | Include all required fields | | `invalid_field` | Field value is invalid | Check field format (e.g., E.164 for phone numbers) | | `invalid_phone_number` | Phone number format invalid | Use E.164 format: +31612345678 | | `invalid_uuid` | Invalid UUID format | Provide a valid UUID | | `invalid_url` | Invalid URL format | Provide a valid HTTPS URL | ### Resource Errors [#resource-errors] | Code | Message | Solution | | ---------------- | ----------------------- | -------------------------------------------------- | | `not_found` | Resource not found | Verify the resource ID exists | | `already_exists` | Resource already exists | The phone number or resource is already registered | | `conflict` | Resource conflict | Another resource is using this identifier | ### Billing Errors [#billing-errors] | Code | Message | Solution | | ---------------------- | ---------------------------- | ------------------------------------ | | `insufficient_credits` | Insufficient credits balance | Add credits in the billing dashboard | | `payment_required` | Payment required | Add credits to make outbound calls | ### Rate Limit Errors [#rate-limit-errors] | Code | Message | Solution | | --------------------- | ------------------------------ | ---------------------------------------- | | `rate_limit_exceeded` | Too many requests | Wait and retry with exponential backoff | | `call_control_limit` | Too many call control commands | Limit to 10 commands per minute per call | ### Provider Errors [#provider-errors] | Code | Message | Solution | | ------------------ | ------------------------ | -------------------------------------------- | | `provider_error` | External provider error | Check BYOK configuration and provider status | | `provider_timeout` | Provider request timeout | Retry the request | | `invalid_api_key` | BYOK API key invalid | Update the API key via BYOK endpoint | ### Call Errors [#call-errors] | Code | Message | Solution | | --------------------- | -------------------------- | --------------------------------------- | | `call_not_found` | Call not found | Verify the call ID | | `call_not_active` | Call is not in-progress | Call control only works on active calls | | `invalid_destination` | Invalid destination number | Check phone number format | *** ## Handling Errors [#handling-errors] ### Example Error Handler (JavaScript) [#example-error-handler-javascript] ```javascript async function callApi(endpoint, options = {}) { const response = await fetch(`https://api.hmsovereign.com/api/v1${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { const error = await response.json(); switch (response.status) { case 401: throw new Error('Invalid API key. Check your credentials.'); case 402: throw new Error('Insufficient credits. Add credits to continue.'); case 404: throw new Error(`Resource not found: ${error.error?.message}`); case 429: // Implement retry with backoff const retryAfter = response.headers.get('Retry-After') || 60; throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`); default: throw new Error(error.error?.message || 'Unknown error'); } } return response.json(); } ``` ### Retry Strategy [#retry-strategy] For transient errors (429, 502, 503, 504), implement exponential backoff: ```javascript async function retryWithBackoff(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; const delay = Math.min(1000 * Math.pow(2, i), 30000); await new Promise(resolve => setTimeout(resolve, delay)); } } } ``` *** ## Related [#related] * [Authentication](../get-started/authentication.md) * [Troubleshooting](../integrations/byok-setup.md) * [Rate Limits](../integrations/byok-setup.md) # Rate Limits Source: https://doc.voicedock.ai/docs/reference/rate-limits HMS Sovereign applies rate limits to ensure fair usage and maintain service quality for all users. ## Current Limits [#current-limits] | Limit Type | Rate | Scope | | ------------ | ------------------- | --------------- | | API Requests | 100 requests/minute | Per API key | | Call Control | 10 commands/minute | Per active call | ## Rate Limit Headers [#rate-limit-headers] Every API response includes headers to help you track your usage: | Header | Description | | ----------------------- | ------------------------------------ | | `X-RateLimit-Limit` | Maximum requests allowed | | `X-RateLimit-Remaining` | Requests remaining in current window | | `X-RateLimit-Reset` | Unix timestamp when the limit resets | ## Exceeding the Limit [#exceeding-the-limit] When you exceed the rate limit, the API returns: ``` HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1702479600 Retry-After: 45 ``` ```json { "error": { "code": "rate_limit_exceeded", "message": "Too many requests. Please retry after 45 seconds." } } ``` ## Best Practices [#best-practices] ### 1. Monitor Rate Limit Headers [#1-monitor-rate-limit-headers] Check headers before making requests: ```javascript const response = await fetch(url, options); const remaining = response.headers.get('X-RateLimit-Remaining'); if (remaining < 10) { console.warn('Approaching rate limit:', remaining, 'requests remaining'); } ``` ### 2. Implement Exponential Backoff [#2-implement-exponential-backoff] When you receive a 429 response: ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status !== 429) { return response; } const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); } throw new Error('Max retries exceeded'); } ``` ### 3. Cache Responses [#3-cache-responses] Cache data that doesn't change frequently: ```javascript const cache = new Map(); const CACHE_TTL = 60000; // 1 minute async function getAgent(agentId) { const cacheKey = `assistant:${agentId}`; const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const response = await fetch(`/assistants/${agentId}`); const data = await response.json(); cache.set(cacheKey, { data, timestamp: Date.now() }); return data; } ``` ### 4. Batch Operations [#4-batch-operations] Instead of multiple individual requests, use batch-friendly patterns: ```javascript // Instead of this: for (const id of agentIds) { const assistant = await getAgent(id); // N requests } // Do this: const assistants = await listAgents(); // 1 request const relevantAgents = assistants.filter(a => agentIds.includes(a.id)); ``` ### 5. Use Webhooks for Real-Time Data [#5-use-webhooks-for-real-time-data] Instead of polling for call status, use webhooks: ```json { "webhook_url": "https://your-domain.com/webhook", "webhook_events": ["status-update", "end-of-call-report"] } ``` ## Call Control Limits [#call-control-limits] Call control commands have a separate limit of 10 commands per minute per active call. This prevents abuse while allowing normal interaction patterns. **Examples that count toward the limit:** * `inject-context` * `say` * `end-call` * `transfer` ## Higher Limits [#higher-limits] If you need higher rate limits for your use case, contact [support@hmsovereign.com](mailto:support@hmsovereign.com) with: * Your organization ID * Expected request volume * Use case description *** ## Related [#related] * [Authentication](../get-started/authentication.md) * [Error Codes](../integrations/byok-setup.md) * [Call Control API](/docs/api/call-control/controlCall) # Troubleshooting Source: https://doc.voicedock.ai/docs/reference/troubleshooting This guide covers common issues you may encounter when integrating with HMS Sovereign and how to resolve them. ## Authentication Issues [#authentication-issues] ### 401 Unauthorized [#401-unauthorized] **Symptom:** All API requests return `401 Unauthorized`. **Possible Causes:** * Missing `Authorization` header * Invalid API key * API key has been revoked **Solution:** 1. Verify your API key is correct 2. Ensure the header format is `Authorization: Bearer YOUR_API_KEY` 3. Check if the key is active in Developer > Your API Key 4. Generate a new key if needed ```bash # Correct format curl https://api.hmsovereign.com/api/v1/account \ -H "Authorization: Bearer fl_live_abc123..." ``` ### 403 Forbidden [#403-forbidden] **Symptom:** Request returns `403 Forbidden`. **Possible Causes:** * Trying to access another organization's resources * Resource doesn't belong to your organization **Solution:** Verify the resource ID belongs to your organization by listing your resources first. *** ## Call Issues [#call-issues] ### Calls Not Being Answered [#calls-not-being-answered] **Symptom:** Incoming calls ring but are not answered by your assistant. **Possible Causes:** * Phone number not registered in HMS Sovereign * No assistant assigned to the phone number * Assistant is set to `is_active: false` * SIP forwarding not configured correctly with your provider **Solution:** 1. Verify the number is registered: ```bash curl https://api.hmsovereign.com/api/v1/numbers \ -H "Authorization: Bearer YOUR_API_KEY" ``` 2. Check an assistant is assigned (`assistant_id` is not null) 3. Verify the assistant is active: ```bash curl https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` 4. Contact your SIP provider to verify forwarding is configured ### Calls Ending Immediately [#calls-ending-immediately] **Symptom:** Calls connect but end within seconds. **Possible Causes:** * Insufficient credits balance * BYOK API keys invalid or expired * LLM provider rate limiting **Solution:** 1. Check your credits balance: ```bash curl https://api.hmsovereign.com/api/v1/account \ -H "Authorization: Bearer YOUR_API_KEY" ``` 2. Verify BYOK keys are valid by testing them directly with the provider 3. Check your provider dashboards for rate limit or billing issues ### Poor Audio Quality [#poor-audio-quality] **Symptom:** Audio is choppy, delayed, or unclear. **Possible Causes:** * Network latency between caller and our servers * TTS provider issues * STT provider struggling with audio input **Solution:** 1. Check your network connectivity 2. Try a different TTS voice or provider 3. Ensure the caller's environment isn't too noisy *** ## Webhook Issues [#webhook-issues] ### Webhooks Not Being Received [#webhooks-not-being-received] **Symptom:** Your endpoint isn't receiving webhook requests. **Possible Causes:** * Incorrect `webhook_url` configured * Webhook events not enabled * Your endpoint is not accessible from the internet * Firewall blocking requests **Solution:** 1. Verify webhook configuration: ```bash curl https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` 2. Check `webhook_url` and `webhook_events` are set correctly 3. Test your endpoint is accessible: ```bash curl -X POST https://your-domain.com/webhook \ -H "Content-Type: application/json" \ -d '{"test": true}' ``` 4. Check your firewall allows incoming POST requests ### Webhook Signature Verification Failing [#webhook-signature-verification-failing] **Symptom:** Your signature verification always returns false. **Possible Causes:** * Using wrong `webhook_secret` * Not using raw request body for verification * Timestamp format mismatch **Solution:** 1. Verify you're using the correct secret from your assistant configuration 2. Use the raw request body (string), not parsed JSON: ```python # Correct payload = request.get_data(as_text=True) # Wrong payload = json.dumps(request.json) # This may reorder keys ``` 3. Ensure timestamp is included in signature calculation ### Assistant Request Timing Out [#assistant-request-timing-out] **Symptom:** Calls proceed with default configuration despite webhook being configured. **Cause:** Your endpoint is taking more than 5 seconds to respond. **Solution:** 1. Optimize your endpoint to respond faster 2. Cache frequently accessed data (CRM lookups, etc.) 3. Use async processing where possible 4. If you can't respond in time, return an empty object `{}` to use defaults *** ## BYOK Issues [#byok-issues] ### Provider Key Not Working [#provider-key-not-working] **Symptom:** Calls fail when using BYOK configuration. **Possible Causes:** * API key is invalid or expired * Key doesn't have required permissions * Provider account has insufficient credits **Solution:** 1. Test the key directly with the provider's API 2. Check key permissions (e.g., OpenAI keys need "All" access) 3. Verify provider account has available credits 4. Re-add the key via the BYOK API: ```bash curl -X POST https://api.hmsovereign.com/api/v1/byok \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"provider": "openai", "api_key": "sk-..."}' ``` ### ElevenLabs Voices Not Working [#elevenlabs-voices-not-working] **Symptom:** Assistant uses a different voice than configured. **Cause:** The voice ID is not in your ElevenLabs Voice Library. **Solution:** 1. Go to [https://elevenlabs.io/app/voice-library](https://elevenlabs.io/app/voice-library) 2. Add the voice you want to use to your library 3. Copy the voice ID from your library (not the public listing) 4. Update your assistant with the correct voice ID *** ## Outbound Call Issues [#outbound-call-issues] ### 402 Payment Required [#402-payment-required] **Symptom:** Outbound calls return `402 Payment Required`. **Cause:** Your organization has insufficient credits balance. **Solution:** Add credits through the billing dashboard or API. See [Billing Guide](../integrations/byok-setup.md). ### Outbound Call Not Connecting [#outbound-call-not-connecting] **Symptom:** Outbound call initiated but destination never rings. **Possible Causes:** * Invalid destination phone number format * Destination number is blocking unknown callers * SIP trunk configuration issue **Solution:** 1. Verify phone number is in E.164 format (e.g., `+31612345678`) 2. Try calling a different test number 3. Check SIP trunk is configured for outbound calls *** ## API Issues [#api-issues] ### 429 Too Many Requests [#429-too-many-requests] **Symptom:** API returns `429 Too Many Requests`. **Cause:** You've exceeded the rate limit (100 requests/minute). **Solution:** 1. Implement exponential backoff in your code 2. Check `X-RateLimit-Remaining` header to track usage 3. Cache responses where appropriate 4. Contact support if you need higher limits ### 400 Bad Request [#400-bad-request] **Symptom:** API returns `400 Bad Request` with validation errors. **Solution:** Read the error message carefully - it indicates which field is invalid: ```json { "error": { "code": "invalid_request", "message": "Missing required field: phone_number", "param": "phone_number" } } ``` Common validation issues: * Phone numbers must be E.164 format: `+31612345678` * UUIDs must be valid format * Required fields cannot be null *** ## Getting Help [#getting-help] If you've tried the solutions above and still have issues: 1. **Check the API response** - Error messages often contain helpful details 2. **Review recent changes** - Did this work before? What changed? 3. **Test with minimal configuration** - Remove optional features to isolate the issue 4. **Contact support** - Email [support@hmsovereign.com](mailto:support@hmsovereign.com) with: * Your organization ID * The request that's failing (redact sensitive data) * The full error response * Timestamp of when the issue occurred # Node.js SDK Source: https://doc.voicedock.ai/docs/sdks/node Official TypeScript/Node.js SDK for the HMS Sovereign Voice AI Platform. Zero dependencies, full type safety, and support for all API endpoints. ## Installation [#installation] ```bash npm install hmsovereign ``` Requires Node.js 20 or later. ## Quick Start [#quick-start] ```typescript import { HmsSovereign } from 'hmsovereign'; const client = new HmsSovereign({ apiKey: 'fl_live_...', }); // List all assistants const assistants = await client.assistants.list(); // Create an assistant const assistant = await client.assistants.create({ name: 'Customer Support', first_message: 'Hello, how can I help you?', llm_config: { provider: 'openai', model: 'gpt-4o-mini', messages: [{ role: 'system', content: 'You are a friendly customer service agent.' }], }, stt_config: { provider: 'deepgram', model: 'nova-3-general', language: 'en', }, tts_config: { provider: 'elevenlabs', voice_id: 'ukiwGs47sHyibruHJ1vg', }, }); ``` *** ## Resources [#resources] All resources are available as properties on the client: | Resource | Description | | -------------------------- | --------------------------------------------- | | `client.assistants` | CRUD for AI voice assistants | | `client.calls` | List calls, make outbound calls, call control | | `client.numbers` | Phone number management | | `client.campaigns` | Outbound call campaigns with leads | | `client.sipTrunks` | SIP trunk configuration | | `client.voices` | List available TTS voices | | `client.usage` | Usage logs and billing | | `client.byok` | Bring Your Own Key management | | `client.toolTemplates` | Reusable tool/function templates | | `client.analysisTemplates` | Post-call analysis schemas | | `client.domains` | Custom domain configuration | | `client.organizations` | Organization management | *** ## Outbound Calls [#outbound-calls] Three configuration modes for outbound calls: ### Reference mode — use a saved assistant [#reference-mode--use-a-saved-assistant] ```typescript const call = await client.calls.create({ destination: '+31612345678', assistant_id: 'uuid-of-saved-assistant', }); console.log(`Call started: ${call.call_id}`); ``` ### Transient mode — one-time assistant config [#transient-mode--one-time-assistant-config] ```typescript const call = await client.calls.create({ destination: '+31612345678', assistant: { first_message: 'Hello John, this is a reminder about your appointment.', llm_config: { provider: 'openai', model: 'gpt-4o-mini' }, stt_config: { provider: 'deepgram', model: 'nova-3-general', language: 'en' }, tts_config: { provider: 'elevenlabs', voice_id: 'ukiwGs47sHyibruHJ1vg' }, }, }); ``` ### Hybrid mode — saved assistant with overrides [#hybrid-mode--saved-assistant-with-overrides] ```typescript const call = await client.calls.create({ destination: '+31612345678', assistant_id: 'uuid-of-saved-assistant', assistant_override: { first_message: 'Good afternoon John, I'm calling about your order.', metadata: { order_id: '12345', customer_tier: 'premium' }, }, }); ``` The `metadata` object is passed through to all [webhook events](/docs/webhooks/overview) at `message.assistant.metadata`. *** ## Call Control [#call-control] Control active calls in real-time: ```typescript // Inject context (invisible to caller, visible to LLM) await client.calls.injectContext(callId, 'Customer is a VIP member', true); // Make the assistant say something await client.calls.say(callId, 'One moment please, let me look that up for you.'); // Transfer to a human agent await client.calls.transfer(callId, '+31201234567', 'I'm transferring you to a colleague.'); // End the call await client.calls.end(callId, 'Thank you for your call. Goodbye!'); ``` *** ## Pagination [#pagination] List endpoints return a `Page` with metadata: ```typescript // Single page const page = await client.calls.list({ status: 'ended', limit: 50 }); console.log(page.data); // Call[] console.log(page.pagination); // { total, limit, offset } console.log(page.hasMore); // boolean ``` ### Auto-pagination [#auto-pagination] Iterate through all results automatically with `listAll()`: ```typescript for await (const call of client.calls.listAll({ status: 'ended' })) { console.log(call.id, call.duration_seconds); } // Or collect everything into an array const allCalls = await client.calls.listAll({ status: 'ended' }).toArray(); ``` Auto-pagination is available on `client.calls.listAll()` and `client.usage.listAll()`. *** ## Campaigns [#campaigns] Run automated outbound call campaigns: ```typescript // Create a campaign with leads const campaign = await client.campaigns.create({ name: 'Appointment Reminders', agent_id: assistant.id, system_message_template: 'Call {{name}} about the appointment on {{date}}', schedule_start_time: '09:00', schedule_end_time: '17:00', timezone: 'America/New_York', leads: [ { phone_number: '+15551234567', name: 'John Smith', variables: { date: '2026-03-25', time: '14:00' }, }, ], }); // Add more leads later await client.campaigns.addLead(campaign.id, { phone_number: '+15559876543', name: 'Peter Johnson', variables: { date: '2026-03-26', time: '10:00' }, }); ``` *** ## Webhook Verification [#webhook-verification] The SDK provides built-in webhook signature verification. See [Webhook Security](/docs/webhooks/security) for background. ```typescript import { Webhooks } from 'hmsovereign'; // Express example — use raw body for signature verification app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { try { const event = Webhooks.verify({ payload: req.body, signature: req.headers['x-webhook-signature'] as string, timestamp: req.headers['x-webhook-timestamp'] as string, secret: process.env.WEBHOOK_SECRET!, }); switch (event.message.type) { case 'assistant-request': // Return dynamic config before call is answered res.json({ assistant: { first_message: 'Welcome!' } }); break; case 'tool-calls': // Handle function calls from the assistant const toolCall = event.message.tool_call_list[0]; res.json({ result: { status: 'ok' } }); break; case 'status-update': // Call state: in-progress, ended, ended-with-error console.log(event.message.call.status); res.sendStatus(200); break; case 'end-of-call-report': // Post-call summary, transcript, analysis console.log(event.message.summary); res.sendStatus(200); break; default: res.sendStatus(200); } } catch (error) { res.status(400).send('Invalid signature'); } }); ``` > **WarningRaw body required**: You must use `express.raw()` (or equivalent) to get the raw request body. Parsed JSON bodies will fail signature verification because the signature is computed over the original string. *** ## Error Handling [#error-handling] The SDK throws typed errors that you can catch with `instanceof`: ```typescript import { AuthenticationError, NotFoundError, RateLimitError, InsufficientCreditsError, } from 'hmsovereign'; try { await client.assistants.get('non-existent-id'); } catch (error) { if (error instanceof NotFoundError) { console.log('Assistant not found'); } else if (error instanceof AuthenticationError) { console.log('Invalid API key'); } else if (error instanceof RateLimitError) { console.log(`Rate limited, retry after ${error.retryAfter}s`); } else if (error instanceof InsufficientCreditsError) { console.log('Top up your credits at app.hmsovereign.com'); } } ``` All errors extend `HmsSovereignError` and include `status`, `message`, and `code` properties. *** ## Configuration [#configuration] ```typescript const client = new HmsSovereign({ apiKey: 'fl_live_...', // Required baseUrl: 'https://...', // Default: https://api.hmsovereign.com/api/v1 maxRetries: 2, // Default: 2 (retries on 5xx and 429) timeout: 30_000, // Default: 30s debug: true, // Default: false — logs requests and responses }); ``` | Option | Default | Description | | ------------ | ------------------------------------ | ---------------------------------------------- | | `apiKey` | — | Your HMS Sovereign API key (required) | | `baseUrl` | `https://api.hmsovereign.com/api/v1` | API base URL | | `maxRetries` | `2` | Automatic retries on 5xx and 429 responses | | `timeout` | `30000` | Request timeout in milliseconds | | `debug` | `false` | Log all HTTP requests and responses to console | The SDK automatically retries failed requests with exponential backoff. On `429` responses, it respects the `Retry-After` header. *** ## TypeScript [#typescript] The SDK is written in TypeScript and exports types for all resources. Import them directly: ```typescript import type { Assistant, Call, Campaign, Lead, PhoneNumber, SipTrunk, Voice, UsageRecord, WebhookPayload, SttConfig, LlmConfig, TtsConfig, } from 'hmsovereign'; ``` *** ## Source & Issues [#source--issues] * GitHub: [github.com/flireo-bv-org/hmsovereign-node](https://github.com/flireo-bv-org/hmsovereign-node) * npm: [npmjs.com/package/hmsovereign](https://www.npmjs.com/package/hmsovereign) * Issues: [github.com/flireo-bv-org/hmsovereign-node/issues](https://github.com/flireo-bv-org/hmsovereign-node/issues) # Build a voice agent Source: https://doc.voicedock.ai/docs/tutorials/build-a-voice-agent A short walkthrough of the assistant builder: how to configure a voice agent from scratch in the VoiceDock dashboard and place a live test call to hear it perform. ## In this video [#in-this-video] * Create and name an assistant, and set the business name and notification emails * Choose between a pipeline (STT + LLM + TTS) and a realtime model such as Gemini Live * Write the system prompt, or generate one from your website with **Generate with AI** * Add tools such as **End call** and call transfers * Configure voice, transcription, silence handling and a call-analysis template * Hear a live web test call handled by Gemini Live 3.1 ## Related [#related] * [Assistants](/docs/core-concepts/assistants) — the concepts behind everything configured here * [AI generation](/docs/features/ai-generation) — how **Generate with AI** builds your prompt * [Web calls](/docs/features/web-calls) — the browser test call shown at the end # Assistant Request Webhook Source: https://doc.voicedock.ai/docs/webhooks/assistant-request Called **before** an inbound call is answered, allowing you to dynamically configure the assistant based on caller information. ## When It's Called [#when-its-called] 1. Inbound call arrives at your phone number 2. Assistant has `webhook_url` configured with `assistant-request` event enabled 3. HMS Sovereign sends POST request to your webhook 4. Your endpoint responds with configuration overrides (or empty object) 5. Call is answered with the configured settings When a number has **no assigned assistant**, the assistant-request is sent automatically to the number's webhook (or, if the number has none, the account webhook) so your server can supply the assistant for the call. In that case you don't need to enable the event explicitly — it's required to route the call. See [Where webhooks are configured](/docs/webhooks/overview) for the full precedence order. ## Timeout [#timeout] Your endpoint must respond within **5 seconds**. If it doesn't respond in time, the call proceeds with the default assistant configuration. ## Request Payload [#request-payload] ```json { "message": { "type": "assistant-request", "timestamp": "2025-12-13T12:00:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "ringing" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" } } } ``` ## Response Options [#response-options] ### Use Default Configuration [#use-default-configuration] Return an empty object to use the default assistant configuration: ```json {} ``` ### Personalize Greeting [#personalize-greeting] ```json { "assistant": { "first_message": "Hello John, great to hear from you! How can I help?" } } ``` ### Override LLM Configuration [#override-llm-configuration] ```json { "assistant": { "llm_config": { "provider": "openai", "model": "gpt-4o", "messages": [ { "role": "system", "content": "You are speaking with John Smith, a VIP customer. Be extra helpful." } ] } } } ``` ### Set Call Duration Limit [#set-call-duration-limit] ```json { "max_duration_seconds": 300 } ``` ### Reject the Call [#reject-the-call] Return an error message to reject the call. The message is spoken to the caller: ```json { "error": "Sorry, we are currently closed. Please call back during business hours." } ``` ### Enable Call Analysis [#enable-call-analysis] ```json { "assistant": { "analysis_plan": { "structured_data_plan": { "enabled": true, "schema": { "type": "object", "properties": { "intent": { "type": "string" }, "sentiment": { "type": "string", "enum": ["positive", "neutral", "negative"] }, "appointment_booked": { "type": "boolean" } } }, "messages": [ { "role": "system", "content": "Analyze the call transcript according to the schema: {{schema}}" }, { "role": "user", "content": "Transcript: {{transcript}}" } ] } } } } ``` ## Use Cases [#use-cases] * **CRM Integration**: Look up caller in your CRM and personalize greeting * **Business Hours**: Reject calls outside business hours * **VIP Handling**: Use premium model for important customers * **A/B Testing**: Route callers to different assistant configurations * **Language Detection**: Adjust language based on caller's region ## Example Implementation (Node.js) [#example-implementation-nodejs] ```javascript app.post('/webhooks/assistant-request', async (req, res) => { const { message } = req.body; const customerNumber = message.customer.number; // Look up customer in CRM const customer = await lookupCustomer(customerNumber); if (!customer) { // New caller - use default return res.json({}); } // Personalize for known customer return res.json({ assistant: { first_message: `Hello ${customer.name}, welcome back! How can I help?`, llm_config: { messages: [ { role: "system", content: `You are speaking with ${customer.name}. Customer since ${customer.since}. ${customer.notes}` } ] } } }); }); ``` See [Assistant Request Webhook API](/docs/api/webhooks/assistantRequestWebhook) for complete schema details. # End of Call Report Webhook Source: https://doc.voicedock.ai/docs/webhooks/end-of-call-report Called after the call ends with a summary and optional structured analysis. This is the most commonly used webhook for CRM integrations. ## When It's Called [#when-its-called] After the call ends, HMS Sovereign: 1. Generates an AI summary of the conversation 2. Runs structured analysis (if `analysis_plan` was configured) 3. Sends the report to your webhook ## Request Payload [#request-payload] ```json { "message": { "type": "end-of-call-report", "timestamp": "2025-12-13T12:05:30.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "ended" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" }, "duration_seconds": 84, "summary": "Customer asked about opening hours and wanted to schedule an appointment for next week.", "analysis": { "sentiment": 8, "primary_topic": "Schedule appointment", "question_answered": true, "conversation_quality": "Good" } } } ``` ## Payload Fields [#payload-fields] | Field | Type | Always Present | Description | | ------------------ | ------- | -------------- | ----------------------------------- | | `duration_seconds` | integer | Yes | Total call duration | | `summary` | string | Yes | AI-generated summary | | `analysis` | object | No | Structured analysis (if configured) | ## Response [#response] The response body is **ignored**. Return any 2xx status code to acknowledge receipt. ## Analysis Configuration [#analysis-configuration] To get structured analysis, configure `analysis_plan` on your assistant: ```json { "analysis_plan": { "structured_data_plan": { "enabled": true, "schema": { "type": "object", "properties": { "sentiment": { "type": "integer", "minimum": 1, "maximum": 10, "description": "Customer sentiment score 1-10" }, "intent": { "type": "string", "enum": ["support", "sales", "complaint", "other"] }, "resolved": { "type": "boolean", "description": "Whether the customer's issue was resolved" }, "follow_up_needed": { "type": "boolean" } } }, "messages": [ { "role": "system", "content": "Analyze the call transcript according to the provided schema. Return valid JSON matching the schema." }, { "role": "user", "content": "Schema: {{schema}}\n\nTranscript: {{transcript}}\n\nCall ended because: {{ended_reason}}" } ] }, "min_messages_threshold": 5 } } ``` ### Analysis Plan Fields [#analysis-plan-fields] | Field | Description | | ------------------------------- | ---------------------------------------- | | `structured_data_plan.enabled` | Enable/disable analysis | | `structured_data_plan.schema` | JSON Schema for output | | `structured_data_plan.messages` | Prompt template with placeholders | | `min_messages_threshold` | Min conversation messages before running | ### Available Placeholders [#available-placeholders] | Placeholder | Description | | ------------------ | -------------------- | | `{{schema}}` | Your JSON Schema | | `{{transcript}}` | Full call transcript | | `{{ended_reason}}` | Why the call ended | ## Use Cases [#use-cases] * Sync call summaries to CRM * Track customer sentiment over time * Identify sales opportunities * Measure call quality * Detect escalation needs * Build analytics dashboards ## Example Implementation (Node.js) [#example-implementation-nodejs] ```javascript app.post('/webhooks/end-of-call-report', async (req, res) => { const { message } = req.body; // Store in database await db.calls.insert({ call_id: message.call.id, customer_phone: message.customer.number, duration_seconds: message.duration_seconds, summary: message.summary, analysis: message.analysis, created_at: message.timestamp }); // Update CRM contact await crm.updateContact(message.customer.number, { last_call_date: message.timestamp, last_call_summary: message.summary, sentiment: message.analysis?.sentiment }); // Check for follow-up needs if (message.analysis?.follow_up_needed) { await tasks.create({ type: 'follow_up_call', phone: message.customer.number, reason: message.summary, due_date: addDays(new Date(), 1) }); } res.status(200).send('OK'); }); ``` See [End of Call Report Webhook API](/docs/api/webhooks/endOfCallReportWebhook) for complete schema details. # Webhooks Overview Source: https://doc.voicedock.ai/docs/webhooks/overview Webhooks allow you to receive real-time notifications about call events. When events occur, HMS Sovereign sends HTTP POST requests to your configured endpoint. ## Available Webhooks [#available-webhooks] | Webhook | Endpoint ID | Description | | ------------------------------------------------------- | ----------- | ---------------------- | | [Assistant Request](/docs/webhooks/assistant-request) | `25570929` | Pre-call configuration | | [Tool Calls](/docs/webhooks/tool-calls) | `25570944` | Function execution | | [Status Update](/docs/webhooks/status-update) | `25570945` | Call lifecycle events | | [End of Call Report](/docs/webhooks/end-of-call-report) | `25570946` | Post-call summary | ## Where webhooks are configured [#where-webhooks-are-configured] Webhook (server) URLs can be set at multiple levels. A more specific level overrides a less specific one, so you can route different traffic to different backends. **Precedence:** tool `url` → assistant `webhook_url` → phone number → account webhook. ### Account level (organization default) [#account-level-organization-default] Set a webhook URL on your account in the dashboard settings. This is the organization-wide default endpoint: any assistant or number that does not define its own webhook falls back to this account-level webhook. It also receives the `assistant-request` event for inbound calls to phone numbers that are not linked to a specific assistant, so you can return the configuration to use for that call. ### Phone number level [#phone-number-level] Set a webhook on an individual number from the dashboard: open the number under **Phone numbers** and fill in the **Webhooks** section — the URL, an optional signing secret, and which events to send. A number's webhook is used when its assigned assistant has no webhook of its own, and takes precedence over the account webhook. It's the natural place for a webhook when a number resolves its assistant dynamically at call time (see [Assistant Request](/docs/webhooks/assistant-request)). ### Agent level (per assistant) [#agent-level-per-assistant] Set `webhook_url` (and `webhook_secret`) on an assistant to receive that assistant's call events. This overrides the number- and account-level webhooks for that assistant. ```bash curl -X PATCH https://api.hmsovereign.com/api/v1/assistants/ASSISTANT_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://api.example.com/webhooks/hms-sovereign", "webhook_secret": "your-secret-for-verification", "webhook_events": ["assistant-request", "tool-calls", "status-update", "end-of-call-report"] }' ``` ### Tool level (per function) [#tool-level-per-function] A [custom tool](/docs/configuration/custom-tools) can define its own `url`. When set, that tool's `tool-calls` go directly to that URL instead of the assistant's `webhook_url` — handy for routing individual functions straight to the right backend (for example `lookup_customer` to your CRM and `book_appointment` to your calendar) without building a central router. ## Request Format [#request-format] All webhooks send POST requests with: ### Headers [#headers] | Header | Description | | --------------------- | --------------------- | | `Content-Type` | `application/json` | | `User-Assistant` | `HMS-Sovereign/1.0` | | `X-Webhook-Event` | Event type | | `X-Webhook-Timestamp` | Unix timestamp | | `X-Webhook-Signature` | HMAC-SHA256 signature | ### Body Structure [#body-structure] ```json { "message": { "type": "status-update", "timestamp": "2025-12-13T12:00:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "in-progress" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" } } } ``` ## Response Requirements [#response-requirements] | Webhook | Response Required | Timeout | | ------------------ | --------------------- | ---------- | | Assistant Request | Yes (config or empty) | 5 seconds | | Tool Calls | Yes (result) | 10 seconds | | Status Update | No (ignored) | - | | End of Call Report | No (ignored) | - | ## Next Steps [#next-steps] * [Assistant Request Webhook](/docs/webhooks/assistant-request) - Dynamic assistant configuration * [Tool Calls Webhook](/docs/webhooks/tool-calls) - Function execution * [Security](/docs/webhooks/security) - Verify webhook signatures # Webhook Security Source: https://doc.voicedock.ai/docs/webhooks/security When you configure a `webhook_secret` on your assistant, HMS Sovereign signs all webhook requests. You should verify these signatures to ensure requests come from HMS Sovereign. ## Signature Format [#signature-format] HMS Sovereign uses HMAC-SHA256 to sign webhooks. The signature is included in the `X-Webhook-Signature` header. ### How It's Calculated [#how-its-calculated] ``` message = timestamp + "." + raw_request_body signature = "sha256=" + HMAC-SHA256(secret, message) ``` The `X-Webhook-Signature` header value is prefixed with `sha256=`. Strip this prefix before comparing with your computed HMAC. ## Verification Examples [#verification-examples] ### Python [#python] ```python import hmac import hashlib def verify_webhook_signature(payload: str, secret: str, timestamp: str, signature: str) -> bool: """ Verify HMS Sovereign webhook signature. Args: payload: Raw request body as string secret: Your webhook_secret from assistant config timestamp: X-Webhook-Timestamp header value signature: X-Webhook-Signature header value (e.g. "sha256=abc123...") Returns: True if signature is valid """ # Strip the "sha256=" prefix from the header value sig_hex = signature.removeprefix("sha256=") message = f"{timestamp}.{payload}" expected = hmac.new( secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(sig_hex, expected) # Flask example from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "your-secret-here" @app.route('/webhooks/hms-sovereign', methods=['POST']) def handle_webhook(): payload = request.get_data(as_text=True) timestamp = request.headers.get('X-Webhook-Timestamp') signature = request.headers.get('X-Webhook-Signature') if not verify_webhook_signature(payload, WEBHOOK_SECRET, timestamp, signature): abort(401, 'Invalid signature') # Process webhook... data = request.json # ... return 'OK', 200 ``` ### Node.js [#nodejs] ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, secret, timestamp, signature) { // Strip the "sha256=" prefix from the header value const sigHex = signature.replace(/^sha256=/, ''); const message = `${timestamp}.${payload}`; const expected = crypto .createHmac('sha256', secret) .update(message) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(sigHex), Buffer.from(expected) ); } // Express example const express = require('express'); const app = express(); const WEBHOOK_SECRET = 'your-secret-here'; app.post('/webhooks/hms-sovereign', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const timestamp = req.headers['x-webhook-timestamp']; const signature = req.headers['x-webhook-signature']; if (!verifyWebhookSignature(payload, WEBHOOK_SECRET, timestamp, signature)) { return res.status(401).send('Invalid signature'); } const data = JSON.parse(payload); // Process webhook... res.status(200).send('OK'); } ); ``` ### Go [#go] ```go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strings" ) func verifyWebhookSignature(payload, secret, timestamp, signature string) bool { // Strip the "sha256=" prefix from the header value sigHex := strings.TrimPrefix(signature, "sha256=") message := timestamp + "." + payload mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(message)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sigHex), []byte(expected)) } ``` ### PHP [#php] ```php max_age_seconds: return False return True ``` See [Webhook Overview](/docs/webhooks/overview) for header details. # Status Update Webhook Source: https://doc.voicedock.ai/docs/webhooks/status-update Called when the call status changes. Use this to track call lifecycle in your systems. ## When It's Called [#when-its-called] The webhook is triggered when the call status changes to: * `in-progress` - Call connected, conversation started * `ended` - Call ended normally (customer hung up) * `ended-with-error` - Call ended due to an error ## Request Payload [#request-payload] ### Call Started (in-progress) [#call-started-in-progress] ```json { "message": { "type": "status-update", "timestamp": "2025-12-13T12:00:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "in-progress" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" } } } ``` ### Call Ended Normally [#call-ended-normally] ```json { "message": { "type": "status-update", "timestamp": "2025-12-13T12:05:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "ended" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" } } } ``` ### Call Ended with Error [#call-ended-with-error] ```json { "message": { "type": "status-update", "timestamp": "2025-12-13T12:05:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "ended-with-error" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" }, "error": "STT connection failed" } } ``` ## Response [#response] The response body is **ignored**. Return any 2xx status code to acknowledge receipt. ``` HTTP/1.1 200 OK ``` ## Use Cases [#use-cases] * Update CRM with call activity * Track active calls in real-time dashboard * Alert on call errors * Log call events for analytics ## Example Implementation (Node.js) [#example-implementation-nodejs] ```javascript app.post('/webhooks/status-update', async (req, res) => { const { message } = req.body; // Update CRM await crm.logActivity({ type: 'call', phone: message.customer.number, status: message.call.status, timestamp: message.timestamp }); // Track active calls if (message.call.status === 'in-progress') { await redis.sadd('active_calls', message.call.id); } else { await redis.srem('active_calls', message.call.id); } // Alert on errors if (message.call.status === 'ended-with-error') { await slack.send({ channel: '#alerts', text: `Call error: ${message.error}` }); } res.status(200).send('OK'); }); ``` See [Status Update Webhook API](/docs/api/webhooks/statusUpdateWebhook) for complete schema details. # Tool Calls Webhook Source: https://doc.voicedock.ai/docs/webhooks/tool-calls Called when the AI assistant needs to execute a custom tool/function during the conversation. ## When It's Called [#when-its-called] 1. Assistant has tools defined in `llm_config.tools[]` 2. During conversation, LLM decides to call a tool 3. HMS Sovereign sends POST request with tool call details 4. Your endpoint returns the result 5. LLM continues conversation with the result ## Timeout [#timeout] * **Sync tools**: Must respond within **10 seconds** * **Async tools**: Respond immediately with 200 OK (fire-and-forget) ## Request Payload [#request-payload] ```json { "message": { "type": "tool-calls", "timestamp": "2025-12-13T12:00:00.000Z", "call": { "id": "5c4d030f-43e3-4e65-899e-8148521e660f", "type": "inbound_phone_call", "status": "in-progress" }, "phone_number": { "number": "+31850835037", "name": "HMS Sovereign Demo" }, "customer": { "number": "+31612345678" }, "tool_call_list": [ { "id": "tool_abc123def456", "type": "function", "function": { "name": "lookup_contact", "arguments": { "query": "John Smith" } } } ] } } ``` ## Response Formats [#response-formats] Multiple response formats are supported: ### Object Format (Recommended) [#object-format-recommended] ```json { "results": [ { "tool_call_id": "tool_abc123def456", "result": { "name": "John Smith", "email": "john@example.com", "phone": "+31612345678" } } ] } ``` ### Simple Format [#simple-format] ```json { "result": { "name": "John Smith", "email": "john@example.com" } } ``` ### Direct Format [#direct-format] Your data directly (no wrapper): ```json { "name": "John Smith", "email": "john@example.com" } ``` ### Error Format [#error-format] ```json { "error": "Contact not found" } ``` ## Defining Tools [#defining-tools] Define tools in your assistant's `llm_config.tools[]`: ```json { "llm_config": { "tools": [ { "name": "lookup_contact", "description": "Look up contact information by name or phone number", "url": "https://api.example.com/tools/lookup", "async": false, "async_response": "Event logged successfully.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Name or phone number to search" } }, "required": ["query"] } } ] } } ``` ### Tool Definition Fields [#tool-definition-fields] | Field | Type | Required | Description | | ---------------- | ------- | -------- | ----------------------------------------------- | | `name` | string | Yes | Unique tool name | | `description` | string | Yes | What the tool does (for LLM) | | `url` | string | No | Per-tool URL (overrides assistant webhook\_url) | | `async` | boolean | No | Fire-and-forget mode (default: false) | | `async_response` | string | No | Message for async tools | | `parameters` | object | No | JSON Schema for inputs | ## Async Tools [#async-tools] For logging or notification tools where you don't need to return a result: ```json { "name": "log_event", "description": "Log an event for analytics", "async": true, "async_response": "Event logged successfully.", "parameters": { "type": "object", "properties": { "event_type": { "type": "string" }, "details": { "type": "string" } } } } ``` The LLM receives the `async_response` immediately while your webhook processes in the background. ## Built-in Tools [#built-in-tools] ### End Call Tool [#end-call-tool] Allow the assistant to end the call: ```json { "type": "end_call" } ``` ### Transfer Call Tool [#transfer-call-tool] Allow the assistant to transfer the call: ```json { "type": "transfer_call", "destinations": [ { "type": "number", "number": "+31612345678", "description": "Sales team", "message": "I'm transferring you to sales." }, { "type": "number", "number": "+31687654321", "description": "Support team", "message": "I'm transferring you to support." } ] } ``` ## Example Implementation (Node.js) [#example-implementation-nodejs] ```javascript app.post('/webhooks/tool-calls', async (req, res) => { const { message } = req.body; const toolCalls = message.tool_call_list; const results = []; for (const toolCall of toolCalls) { const { id, function: fn } = toolCall; if (fn.name === 'lookup_contact') { const contact = await db.contacts.findOne({ $or: [ { name: { $regex: fn.arguments.query, $options: 'i' } }, { phone: fn.arguments.query } ] }); results.push({ tool_call_id: id, result: contact || { error: 'Contact not found' } }); } if (fn.name === 'book_appointment') { const appointment = await calendar.createAppointment({ date: fn.arguments.date, time: fn.arguments.time, customerPhone: message.customer.number }); results.push({ tool_call_id: id, result: { success: true, appointmentId: appointment.id, confirmation: `Appointment scheduled for ${fn.arguments.date} at ${fn.arguments.time}` } }); } } return res.json({ results }); }); ``` See [Tool Calls Webhook API](/docs/api/webhooks/toolCallsWebhook) for complete schema details.